Preview of main at d0a501a, 2026-11-07

webcpp::document brings JavaScript programmers the wonders of C++ without the risks of C++. It is a header-only JSON document for C++20, one of the libraries of webcpp, whose behaviour is JavaScript’s: what JSON.parse, property access, assignment, delete, the array methods and JSON.stringify do in JavaScript, document does in C++, with the same key order and the same text out, so that a JavaScript peer and a C++ program agree byte for byte.

Its tree lives in one relocatable image, built through an allocator, so that a document can live in a memory segment, be made durable, be shared between threads and processes, and travel as binary and be opened where it lands, with no parse. It reads and writes JSON and NDJSON streams, keeps numbers exact, seals images with a CRC, and answers queries over ordered keys, from C++ and from JavaScript. Its core never throws: every operation that can fail returns its error as a value.

This page teaches document concept by concept, from JavaScript’s side: what each part does, its smallest example, its variations and its rules. Every C++ program it shows was compiled and run, natively and, where the program does not need Boost.Asio, Boost.Cobalt, threads, shared memory or xactor, on emscripten and for wasm32-wasip2 and wasm32-wasip3 too (Testing), and printed the output shown under it, which is what Node prints for the same JavaScript wherever the page does not say otherwise. The listings of the JavaScript module are copied from the tests that run it. The reference that closes it lists every name the headers declare.

Introduction

webcpp::document brings JavaScript programmers the wonders of C++ without the risks of C++.

The wonders are what JavaScript does not give:

  • JSON and NDJSON streams, read as the bytes arrive, in chunks of any size (Streams of documents).

  • Exact numbers: a number is stored as the decimal that was written, and getDecimal reads it back in full (Numbers).

  • Images sealed by a CRC, so that a byte changed by accident is refused when an image is opened (Sealed images).

  • Memory you manage: one relocatable image, reserve and capacity, shrink_to_fit and compact, pools of blocks, and no garbage collector in the way (Images).

  • Shared documents, counted by reference and copied on write, between threads and between processes (Sharing).

  • Binary transmission: an image is sent as it is and opened in place, with no parse and no allocation on receipt (Images on the wire).

  • Ordered-index queries: lower bounds, equal ranges and prefix queries over an object’s keys, with no sorting (Keys in order).

The risks are the ones of C++: a pointer that dangles, a buffer that overflows, behaviour that is undefined. document leaves them out. A handle never fails, a write completes or changes nothing, the core never throws, and a full buffer is an error value: a failure is a value the program tests, and not a crash (Handles).

Where it wins outright is binary transmission, where moving an image costs nothing or a memory copy: between a page and its Worker, between the threads of a process, through shared memory between processes, and from a socket into a pool block on a machine or a fast local link whose receiver’s CPU is the bottleneck (What C++ gives JavaScript). It is honest about where it loses. It parses more slowly than Boost.JSON. An image is many times larger than its JSON text, so over a network the JSON text, compressed, is the smaller payload. And a seal is a CRC against accidental damage, not authentication: never open an image from a source you do not trust (Where document does not win).

The JavaScript programmer also keeps JavaScript’s ways with JSON. A web application speaks JSON with JavaScript, and JavaScript decides what that JSON means: the order in which an object’s keys come out, what JSON.stringify writes for a number, what splice removes. A C++ program that is to agree with a JavaScript peer byte for byte needs a JSON document that makes the same decisions. document is that document: what JSON.parse, property access, assignment, delete, the array methods and JSON.stringify do in JavaScript, it does in C++, with the same key order and the same text out. Its module for JavaScript is taught in Using it from JavaScript.

Why not Boost.JSON

Boost.JSON is the JSON library of Boost, and document reads JSON with its parser. Its document, boost::json::value, is another matter, for the closer compatibility with JavaScript that document was written for. Measured on Boost 1.92:

  • Key order. boost::json::object keeps keys in the order they were inserted. JavaScript puts the keys that are array indices first, ascending, then the others in the order they were created: JSON.parse('{"b":1,"2":2,"a":3,"1":4}') holds {"1":4,"2":2,"b":1,"a":3} (Key order).

  • Erasing reorders. object::erase moves the last member into the erased one’s place: erasing a from {"a":1,"b":2,"c":3,"d":4} leaves {"d":4,"b":2,"c":3}, where JavaScript’s delete leaves {"b":2,"c":3,"d":4}.

  • Numbers are doubles. 0.1000000000000000000000000000000001 is read into a double and written back as 1E-1. JavaScript rounds it too, to 0.1; document keeps it exact by default, and writes every number as JavaScript writes it (Numbers).

  • Hash flooding. An object’s index hashes its keys, so a crafted input of 4 MB, keys that collide, took 9.23 s to parse. Keys come from the network; document indexes them in ordered trees, which no input degrades.

  • Pointers. A boost::json::value holds pointers, so it can neither live in a memory segment shared between processes nor travel as binary. document’s tree lives in one relocatable image, every pointer in it an offset (Images).

document uses Boost.JSON’s SAX parser, boost::json::basic_parser, and nothing else of it. Compared with Boost.JSON puts the two side by side, program by program, and says what Boost.JSON does better.

A first document

This program parses a JSON text, reads two members, writes one, pushes an element onto an array, and writes the document back with an indent of 2. It begins with the library’s header and an alias for its namespace:

#include <webcpp/document.hpp>

#include <cstdint>
#include <iostream>
#include <string_view>

namespace wd = webcpp::document;

wd::parse is JSON.parse. Its result holds the document, or the error that made none: the core of document never throws, and every operation that can fail returns wd::result, Boost.System’s result:

// JSON.parse, whose result holds the document or the error.
wd::result<wd::document> made =
    wd::parse(R"({"name":"Ada","born":1815,"langs":["Analytical"]})");
if (made.has_error()) {
    std::cout << "error: " << made.error().message() << '\n';
    return 1;
}
wd::document& doc = *made;

doc["name"] is a handle, the place doc.name names. value_or is JavaScript’s ??: the value as the type of the fallback, or the fallback when nothing is there, or when what is there is not of that type:

// doc.name ?? "nobody", and doc.born ?? 0.
std::cout << doc["name"].value_or(std::string_view("nobody")) << " was born in "
          << doc["born"].value_or(std::int64_t{0}) << '\n';

set is assignment, and push is Array.prototype.push. Each returns its result, which converts to true on success; a write that fails changes nothing:

// doc.known = true; doc.langs.push("Note G"); each write returns its result.
if (!doc["known"].set(true) || !doc["langs"].push("Note G")) {
    std::cout << "error: the document is full\n";
    return 1;
}

stringify(2) is JSON.stringify(doc, null, 2):

// JSON.stringify(doc, null, 2).
std::cout << doc.stringify(2) << '\n';

It prints:

Ada was born in 1815
{
  "name": "Ada",
  "born": 1815,
  "langs": [
    "Analytical",
    "Note G"
  ],
  "known": true
}

The new member known comes last, as JavaScript adds it, and the text is byte for byte what Node writes for the same operations.

Requirements

  • A C++20 compiler. document’s tests and examples are built and run natively with Clang and GCC, on emscripten under node, and for wasm32-wasip2 and wasm32-wasip3 with the Clang of wasi-sdk, under wasmtime (Testing).

  • Boost 1.92 or newer: Interprocess for the image’s heap and its offsets, MultiIndex for the tree’s indices, Decimal for exact numbers, System for result and the error codes, Charconv for reading and writing doubles, and JSON for its parser. document is header-only; of Boost, only JSON’s parser and Charconv are compiled, once, by webcpp’s build. A b2 project uses document through /webcpp/document//document, which brings all of them.

  • webcpp’s configuration, config, whose <webcpp/config.hpp> every header of document includes first.

  • Exceptions are the program’s choice. The core never throws, in a build with exceptions or without; the sugar that throws as JavaScript does exists only with them (The sugar that throws).

  • Optional: Boost.Asio, for the readers and writers of streams and images, and Boost.Cobalt for its coroutines, in <webcpp/document/asio.hpp>, which builds natively only (With Boost.Asio); xactor for the actor example (In an actor system); Emscripten, for the JavaScript module (Using it from JavaScript).

  • Nothing else: no threads, no clock, no file, no global or static mutable state.

Reading this page

<webcpp/document.hpp> includes every public header, whose names are in the namespace webcpp::document. The code of this page writes them wd::parse and wd::document, as after namespace wd = webcpp::document;.

What C++ gives JavaScript lists what C++ gives JavaScript, and Using it from JavaScript teaches the module that brings it to a page, a Worker and Node. From JavaScript sets JavaScript’s ways with JSON beside document’s, and lists where the two differ on purpose. Handles teaches the handles that read and write a document; Numbers, Strings, dates and bytes and Key order the values they hold and the order of an object’s keys; Arrays the array methods. Writing JSON, Parsing, Streams of documents and Selectors write and read JSON text, whole or in pieces, and Containers shows the containers a C++ program walks. Traits chooses what a document stores; Images shows the binary image, Images on the wire sends it, and Sharing shares it. Compared with Boost.JSON compares document with Boost.JSON, The sugar that throws teaches the sugar that throws, and Errors lists every error. Testing shows how document is tested, Benchmarks what it costs, and the reference closes the page.

What C++ gives JavaScript

webcpp::document brings JavaScript programmers the wonders of C++ without the risks of C++. A JavaScript programmer who reaches for C++ gets control over memory, exact arithmetic, shared memory and bytes that move without a parse, and pays for them with pointers that dangle, buffers that overflow and behaviour that is undefined. document keeps the first and refuses the second: a handle never fails, a write completes or changes nothing, the core never throws, and a full buffer is an error value (Handles).

This section lists what document gives that JavaScript does not, each with the smallest program that shows it and a link to the section that teaches it. The programs are C++; the same document is reached from JavaScript by the module of Using it from JavaScript, which gives a page or a Worker the points that need no C++ to write. Binary transmission comes first, because it is where document wins outright, and the last subsection says where it does not.

Binary transmission

A document is its own wire format. doc.image() is its bytes, and a receiver opens them where they landed: wd::open(buffer), or Document.open(bytes) in JavaScript, reads a header of 64 bytes and, for a sealed image, a CRC, and the document is there. Nothing is parsed and no tree is built, and the receiver allocates nothing per value and nothing per message. A JSON text must be parsed on arrival, and a parse allocates for every value it holds.

That is where document wins outright, in the places where moving the image costs nothing or a memory copy:

  • A page and its Worker. The image crosses as a transferred ArrayBuffer, which moves in constant time, where postMessage of an object makes a structured clone of every node (An image sent to a Worker).

  • Threads of one process. A document moves through a queue as a small handle, and its image stays where it is (Threads).

  • Processes of one machine. A pool of blocks in shared memory, and the offset of a block sent through a queue (Processes).

  • A socket, on one machine or a fast local link. A reader fills a block of a pool straight from the stream and opens the image there, so the receiver’s CPU, which is the bottleneck on such a link, spends none of its time parsing (Images on the wire).

This program copies an image into a buffer of the program’s own and opens it there, with no parse; Boost.JSON, for which a value points into its memory resource, can only be serialized and parsed again:

constexpr std::string_view text = R"({"user":"Ada","langs":["Analytical","Note G"]})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
    return false;
}
const std::span<const std::byte> image = made->image();
// A buffer of the program's, 16-aligned, large enough for the image.
alignas(16) std::array<std::byte, 8192> buffer{};
if (image.size() > buffer.size()) {
    return false;
}
std::ranges::copy(image, buffer.begin());
// Opened in place: the buffer becomes the document, with no parse.
wd::result<wd::basic_document<wd::default_traits, wd::external>> opened =
    wd::open(std::span<std::byte>(buffer.data(), image.size()));
if (opened.has_error()) {
    return false;
}
std::cout << "5. An image copied to another buffer and opened in place\n";
print_document(opened->stringify());
// A Boost.JSON value points into its memory resource, so its bytes cannot be copied: the
// only way to move it is to serialize it and parse it again.
print_boost(boost_text(made->stringify()));
5. An image copied to another buffer and opened in place
  document:   {"user":"Ada","langs":["Analytical","Note G"]}
  Boost.JSON: {"user":"Ada","langs":["Analytical","Note G"]}

Over a network the text is the smaller payload. An image is several times the size of the JSON text it holds, since it keeps a node of 192 bytes for each value (Memory per value), and it is still larger once both are compressed (The sizes on the wire). On a link that carries the bytes, compressed JSON is smaller and arrives sooner, unless the receiver’s parse costs more than the extra bytes take to cross.

JSON and NDJSON streams

JavaScript reads one JSON text with JSON.parse, and reads a stream by cutting it into texts first. wd::stream_parser takes the bytes of a stream as they arrive, in chunks of any size, split anywhere, and delivers each document as soon as the bytes that end it are in: NDJSON, JSON text sequences, and concatenated JSON with or without whitespace between the documents (Streams of documents). A bad line can be dropped, with its line number reported, and the stream goes on (Errors). Reading NDJSON by hand takes a loop over next():

/**
 NDJSON: each document ends at its line's newline, so a line may end in CRLF, a line may be empty,
 and the last line may lack its newline, which finish closes.
*/
bool read_ndjson() {
    std::cout << "NDJSON, taken with next():\n";
    wd::stream_options options;
    options.framing = wd::framing::lines;
    wd::stream_parser p(options);
    constexpr std::string_view text =
        "{\"id\":1,\"tags\":[\"a\",\"b\"]}\n"
        "{\"id\":2,\"note\":\"first\\nsecond\"}\r\n"
        "\n"
        "[3,4.5]\n"
        "null";
    if (!write_in_pieces(p, text, options.on_error)) {
        return false;
    }
    if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
        report_error(p, finished.error());
        return false;
    }
    take_ready(p, options.on_error);
    return true;
}
NDJSON, taken with next():
{"id":1,"tags":["a","b"]}
{"id":2,"note":"first\nsecond"}
[3,4.5]
null

The same parser reads from Boost.Asio, Boost.Cobalt and an actor system, and a JavaScript sender can feed it chunk by chunk (With Boost.Asio, Streams).

Exact numbers

A JavaScript number is a double, and 0.1 + 0.2 is 0.30000000000000004. document stores a number as the decimal that was written, in a decimal128, writes it as JavaScript writes a number, and leaves the arithmetic to the program: in the traits’ own number type it is exact, and get<T>() refuses to round. js_traits stores doubles, to read a number as JavaScript would (Numbers):

// 0.1 + 0.2, computed by the program in the traits' own number type, then stored.
wd::result<wd::document> exact = wd::parse(R"({"a":0.1,"b":0.2})");
wd::result<wd::basic_document<wd::js_traits>> js =
    wd::parse<wd::js_traits>(R"({"a":0.1,"b":0.2})");
if (exact.has_error() || js.has_error()) {
    return 1;
}
using decimal = boost::decimal::decimal128_t;
const wd::result<decimal> a = (*exact)["a"].get<decimal>();
const wd::result<decimal> b = (*exact)["b"].get<decimal>();
const wd::result<double> x = (*js)["a"].get<double>();
const wd::result<double> y = (*js)["b"].get<double>();
if (!a || !b || !x || !y || !(*exact)["sum"].set(*a + *b) || !(*js)["sum"].set(*x + *y)) {
    return 1;
}
std::cout << exact->stringify() << '\n' << js->stringify() << '\n';
{"a":0.1,"b":0.2,"sum":0.3}
{"a":0.1,"b":0.2,"sum":0.30000000000000004}

A JavaScript peer reads the exact digits of a number back with getDecimal, where get gives the Number that rounds them (Exact numbers).

Sealed images

An image is a function of the operations that made it, the same bytes on every target. seal() writes the CRC-32C of those bytes into its header, and opening checks it, so a bit flipped on a disk or a wire, a torn write or a stray write is refused as errc::damaged_document before any of the image is read (Sealed images):

// A sealed image with one byte changed is refused as damaged, by open and by from_image.
const auto found =
    std::ranges::search(bytes, std::as_bytes(std::span(std::string_view("Ada"))));
if (found.empty()) {
    return false;
}
found.front() = std::byte{'a'};
if (!refused("one byte changed", wd::open(bytes))) {
    return false;
}
one byte changed: damaged_document, a sealed image whose bytes do not match its seal

A seal is a CRC against accidental damage and is not authentication: anyone can compute a valid seal for any bytes. Never open an image from a source you do not trust; data from an untrusted source enters as JSON text, which the parser checks entirely (What is refused).

Memory you manage

A document lives in one block of memory, a single relocatable image, and there is no garbage collector in the way: the program decides when the memory grows, when it shrinks and when it goes. reserve grows the image once for a batch (the way std::vector::reserve does) and capacity says how large it is; shrink_to_fit gives back the free space at its end, and compact rewrites the image without the holes that deleted values leave (Compacting); free() in JavaScript releases a document at once, where the collector would. A pool of blocks, allocated once, leases a block to each image that arrives and takes it back, zeroed, when the document goes (Images on the wire):

wd::result<wd::document> made = wd::parse("{}");
if (made.has_error()) {
    return false;
}
for (std::uint64_t i = 0; i < 1000; ++i) {
    if (!(*made)[std::to_string(i)].set(std::string(100, 'x'))) {
        return false;
    }
}
const std::uint64_t grown = made->image().size();
if (!made->root().set(wd::object{{"kept", true}}) || !made->shrink_to_fit()) {
    return false;
}
std::cout << "grown to " << grown << " bytes, shrunk to " << made->image().size() << ": "
          << made->stringify() << '\n';

// Blocks never move: a deep copy into a new document packs the content.
wd::result<wd::document> packed = wd::document::create();
if (packed.has_error() || !packed->root().set(*made)) {
    return false;
}
std::cout << "packed: " << packed->image().size() << " bytes\n";
grown to 524288 bytes, shrunk to 352864: {"kept":true}
packed: 4096 bytes
/** A pool of two blocks: the most the client leases at once. */
wd::image_pool pool_of_two() {
    return wd::image_pool::create(block_bytes, 2).value();
}

Shared documents with copy on write

wd::shared_document is std::shared_ptr for a document: copies share one image, a count in the block’s header says how many hold it, and the last one gives the block back. A holder that wants to write calls make_mutable, which copies the image into a block of its own when others hold it, so the others keep the document as it was. The count lives in the pool, so it is the same in every thread and every process that maps it (Sharing):

// make_mutable copies the image into a block of the writer's own when other holders exist, so the
// writer changes its copy and the others keep the document as it was. The library does not
// synchronise writers: a block with several writers, or a writer and readers of that same block,
// must wait for each other by the program's own means, such as a mutex. This writer changes only
// its copy, which no other holder reads.
void writer_task(wd::shared_document<> holder, std::string& out) {
    if (holder.make_mutable().has_error() || holder["name"].set("Lovelace").has_error()) {
        out = "error";
        return;
    }
    out = describe(holder);
}
writer: Lovelace was born in 1815
after the writer: Ada was born in 1815, use_count 1, 6 of 7 blocks free
last copy dropped: 7 of 7 blocks free

Ordered-index queries

An object is kept in two orders at once: JavaScript’s, which iteration follows, and key order, which by_key() gives with lower_bound, upper_bound and equal_range. A prefix query is two bounds, and nothing is sorted: a JavaScript program sorts every key to get the same answer, and gets it again at each query (Keys in order):

// by_key is the same object in key order, by UTF-16 code units, with bounds for ranges of keys.
// A JavaScript program sorts every key to get any of these; here the tree keeps them sorted.
wd::result<wd::document> catalog_made = wd::parse("{}");
if (catalog_made.has_error()) {
    return fail(catalog_made);
}
wd::document& catalog = *catalog_made;
wd::result<wd::object_ref> as_catalog = catalog.root().as_object();
if (as_catalog.has_error()) {
    return fail(as_catalog);
}
const wd::object_ref catalog_obj = *as_catalog;
const std::array<std::string_view, 13> names{
    "user_3", "admin",  "user_1", "guest",  "user_9", "user",   "user_2",
    "owner",  "user_7", "user_5", "user_4", "user_8", "user_6",
};
for (const std::string_view key : names) {
    const auto added = catalog_obj.insert(key, true);
    if (added.has_error()) {
        return fail(added);
    }
}
std::cout << "JavaScript order:";
for (const auto& member : catalog_obj) {
    std::cout << ' ' << member.first;
}
std::cout << '\n';
const auto keys = catalog_obj.by_key();
std::cout << "by_key order:";
for (const auto& member : keys) {
    std::cout << ' ' << member.first;
}
std::cout << '\n';

// The keys with the prefix "user_" lie from its first key up to the first key at or after
// "user`", the character after '_'.
const std::ranges::subrange users(keys.lower_bound("user_"), keys.lower_bound("user`"));
std::cout << "prefix user_: " << std::ranges::distance(users) << " keys, from "
          << users.begin()->first << " to " << std::ranges::prev(users.end())->first << '\n';

const auto [exact, exact_end] = keys.equal_range("user_5");
std::cout << "equal_range user_5: " << std::ranges::distance(exact, exact_end) << " member, "
          << exact->first << '\n';

std::cout << "lower_bound p: " << keys.lower_bound("p")->first << '\n';
const std::ranges::subrange between(keys.lower_bound("g"), keys.lower_bound("v"));
std::cout << "keys from g up to v: " << std::ranges::distance(between) << '\n';
JavaScript order: user_3 admin user_1 guest user_9 user user_2 owner user_7 user_5 user_4 user_8 user_6
by_key order: admin guest owner user user_1 user_2 user_3 user_4 user_5 user_6 user_7 user_8 user_9
prefix user_: 9 keys, from user_1 to user_9
equal_range user_5: 1 member, user_5
lower_bound p: user
keys from g up to v: 12

Where document does not win

document is a different trade from Boost.JSON and from JavaScript’s own objects, and some of it is a loss:

  • Parsing is slower than Boost.JSON’s and than JSON.parse. Every value is a node of a tree of indices inside the image, which the parse must build; Boost.JSON appends to contiguous arrays. The benchmarks measure it, natively, in Node and in Chrome (Reading the results).

  • An image is larger than its JSON text, many times larger, and still larger when both are compressed. On a narrow network, compressed JSON is the smaller payload and the better choice (The sizes on the wire).

  • A seal is not authentication. It is a CRC against accidental damage. An image is trusted input, and an image from a source that is not trusted must never be opened (What is refused).

  • A lookup walks a tree where JavaScript and Boost.JSON hash, and an array splice renumbers the positions after it (Reading the results).

What document asks for in return is what the other sections teach: a document that behaves as JavaScript does, that moves as bytes, that lives in memory its program manages, and that cannot make the program crash.

Using it from JavaScript

document is also a JavaScript module. js/webcpp-document.mjs is an ES module that wraps the library, compiled to WebAssembly with Emscripten, in two classes, Document and StreamParser, and runs in Node, in a page and in a Worker. A JavaScript program keeps JavaScript’s values and JavaScript’s errors, and gets what JavaScript lacks: exact numbers, queries over ordered keys, a binary image it can send to a Worker with no parse, and memory it frees when it chooses. This section teaches the module; the sections before it teach the C++ library behind it.

The listings of this section are literal, and are not compiled by this page: each is copied from a test or from the example of libs/document/js, where Node’s test runner runs the tests and a headless Chromium runs the example, so that what they show is checked. The files are named above each listing.

The package is built, and its tests run, with b2 and Emscripten’s toolset; the module is the webcpp-document-core program, an ES module, which webcpp-document.mjs imports and awaits when it loads:

b2 toolset=emscripten libs/document/js//package
b2 toolset=emscripten libs/document/test/js//js-tests

The tests are the own lane js of the library (libs/document/test/js), which runs on Emscripten’s toolset: js-tests and shared-tests run Node’s test runner on the packages, npm-package-test tests the npm package as a user receives it, and js-chrome and shared-chrome run the examples in headless Chromium.

Documents

A Document is made by Document.parse(text), which is JSON.parse and throws the SyntaxError that JSON.parse throws, or by Document.open(bytes) (Images). A place in the document is a path: an array of string keys and non-negative integer indices from the root, [] being the root itself. A path that leads to nothing reads undefined, as property access does. get, set, erase, push, splice, length and stringify do what their names do in JavaScript:

Copied from js/test/api.test.mjs
const doc = Document.parse('{"user":{"name":"Ada","age":36,"langs":["C"]},"n":null,"t":false}')
assert.equal(doc.get(['user', 'name']), 'Ada')
assert.equal(doc.get(['user', 'age']), 36)
assert.deepEqual(doc.get(['user', 'langs']), ['C'])
assert.equal(doc.get(['n']), null)
assert.equal(doc.get(['missing', 'deeper']), undefined)
assert.equal(doc.get(['user', 'langs', 0]), 'C')
doc.free()

get returns a scalar as its JavaScript value and a container as a copy, and toJSON() is get([]), so JSON.stringify(doc) works on a document. set writes what JSON.stringify writes for its value, so undefined erases a member, and -0 is 0. erase says whether the key was there:

Copied from js/test/api.test.mjs
const doc = Document.parse('{"user":{"name":"Ada"},"langs":[]}')
doc.set(['user', 'age'], 30)
doc.set(['user', 'tags'], { a: [1, 'two', null] })
doc.set(['pi'], 3.14159)
doc.set(['zero'], -0)
assert.equal(doc.get(['user', 'age']), 30)
doc.set([], [true])
assert.equal(doc.stringify(), '[true]')
doc.free()

push returns the new length, as Array.prototype.push does, and splice takes the arguments of Array.prototype.splice, negative positions and clamped counts included, and returns the elements it removed:

Copied from js/test/api.test.mjs
const doc = Document.parse('{"langs":["C","Rust","Go"]}')
assert.equal(doc.push(['langs'], 'Note G'), 4)
assert.equal(doc.length(['langs']), 4)
assert.deepEqual(doc.splice(['langs'], 0, 1), ['C'])
assert.deepEqual(doc.splice(['langs'], -1, 1, 'JS', 'Lisp'), ['Note G'])
assert.equal(doc.stringify(), JSON.stringify({ langs: ['Rust', 'Go', 'JS', 'Lisp'] }))
doc.free()

stringify(indent) is JSON.stringify(doc, null, indent), with the same bytes, and the key order is JavaScript’s (Key order).

Errors

An operation that fails throws the error JavaScript would throw for it: SyntaxError for malformed text, RangeError for a number out of range or for no room left in a buffer, and TypeError for a value of another kind, a write through something that is not a container, a value that is not JSON (a function, a symbol, a BigInt), a path that is not an array of keys and indices, bytes that are not an image, a sealed image that is damaged, and a document that was freed. These are the errc values of Errors, each under the name JavaScript gives it. An operation that throws changes nothing:

Copied from js/test/api.test.mjs
const doc = Document.parse('{"obj":{"a":1},"arr":[1,2],"num":5,"str":"s"}')
assert.throws(() => doc.set(['missing', 'x'], 1), TypeError)
assert.throws(() => doc.push(['obj'], 1), TypeError)
assert.throws(() => doc.erase(['arr', 0]), TypeError)
assert.throws(() => doc.splice(['num'], 0, 1), TypeError)
assert.throws(() => doc.set(['f'], () => 1), TypeError)
assert.throws(() => doc.get(['arr', 1.5]), TypeError)
doc.free()

Exact numbers

get gives the Number of a JSON number, which rounds a number that a double cannot hold. getDecimal gives the number exactly, as the text of its decimal, so a price or an identifier of thirty digits survives a round trip through a JavaScript program (Numbers):

Copied from js/test/api.test.mjs
const text = '0.1234567890123456789012345678901234'
const doc = Document.parse(`{"price":${text},"small":1.50}`)
assert.equal(doc.getDecimal(['price']), text)
assert.equal(doc.get(['price']), Number(text))
assert.equal(doc.getDecimal(['small']), '1.5')
assert.equal(doc.get(['small']), 1.5)
doc.free()

Ordered keys

The keys of an object are also kept in key order, so a range of keys is found without sorting them. keysInRange(path, low, high) gives the keys from low up to, and not including, high; a prefix query is the range from the prefix to the prefix followed by ￿ (Keys in order):

Copied from js/test/api.test.mjs
const doc = Document.parse('{"users":{"user_2":{},"user_10":{},"admin":{},"user_1":{},' +
    '"user_":{},"user`":{}}}')
assert.deepEqual(doc.keysInRange(['users'], 'user_', 'user_￿'),
    ['user_', 'user_1', 'user_10', 'user_2'])
assert.deepEqual(doc.keysInRange(['users'], 'x', 'y'), [])
doc.free()

Images

A document is its image, and the module hands the image to the program. doc.byteLength is its size, and doc.image() is a Uint8Array that views the bytes where they are in WebAssembly memory, with no copy: it stays valid until the memory grows or the document changes, so a program that keeps the bytes copies them with slice(). Document.open(bytes) makes a document of an image. When the bytes are a view of WebAssembly memory, 16-aligned, such as another document’s image(), it opens them in place and shares them; otherwise it copies them into the module’s memory first, which is a memory copy and no parse. A document opened from bytes is its own, and the bytes it was opened from may change or go. seal() writes the CRC of the image into it, which open checks, and refuses a sealed image whose bytes changed with a TypeError (Sealed images):

Copied from js/test/api.test.mjs
const doc = Document.parse('{"a":[1,2,3]}')
assert.equal(doc.byteLength, doc.image().byteLength)
assert.equal(doc.seal(), undefined)
const image = doc.image().slice()
assert.equal(Document.open(image).stringify(), '{"a":[1,2,3]}')
image[image.length - 1] ^= 0xff
assert.throws(() => Document.open(image), TypeError)
doc.free()

An image is trusted input. open refuses bytes that are not an image, and a seal catches bytes that changed by accident; it does not authenticate them, and an image from a source that is not trusted must never be opened (What is refused).

Memory

A document lives in WebAssembly memory, outside the heap that JavaScript’s collector manages, in one relocatable image. Memory is therefore a thing the program holds, not a thing the collector finds:

  • doc.free() releases the image at once, and a second free() does nothing. A freed document refuses every operation with a TypeError. A program that makes many documents, such as one per message of a stream, frees each when it is done.

  • A document that is never freed is released when the collector collects its object, through a FinalizationRegistry, which runs when the engine chooses and not when the program needs the memory. It is a safety net and not a plan.

  • The module’s memory grows on demand, up to 4 GB, and WebAssembly never returns memory to the system; a freed image returns to the module’s allocator, for the next document to use.

  • image() is a view of that memory and byteLength its size, so a program knows what a document costs, and a document that is to travel as bytes is already the bytes.

A parsed document starts at 4096 bytes and grows as it needs. doc.capacity is the number of bytes its image can hold before a change must move it, and doc.reserve(bytes) makes the image hold at least that many, so a program that knows how large a document will grow pays for the room once (Reserving). A size that is not a non-negative integer, or that is past the document’s limit, is a RangeError, and nothing changes. shrink_to_fit and compact of the C++ library, which pack an image, are not in the module.

Copied from js/test/api.test.mjs
const doc = Document.parse('{"a":1}')
const before = doc.capacity
assert.ok(before >= doc.byteLength)
assert.equal(doc.reserve(before * 4), undefined)
assert.ok(doc.capacity >= before * 4)
assert.equal(doc.stringify(), '{"a":1}')
doc.free()

An image sent to a Worker

A page parses a document, copies its image into an ArrayBuffer of its own and transfers the buffer to a Worker. The transfer moves the buffer in constant time and detaches it from the sender, where postMessage of an object would clone every value of it. The copy is made first because a view of WebAssembly memory cannot be transferred. The Worker opens the image in its own memory, which is one memory copy, and reads from it: nothing is parsed there. The page is js/example/page.mjs:

Copied from js/example/page.mjs
const doc = Document.parse(text)
doc.set(['user', 'age'], 36)
const bytes = doc.image().slice()
doc.free()

worker.postMessage({ image: bytes.buffer }, [bytes.buffer])

and the Worker, js/example/worker.mjs, which answers with what the document holds:

Copied from js/example/worker.mjs
self.onmessage = ({ data }) => {
    const doc = Document.open(new Uint8Array(data.image))
    self.postMessage({
        name: doc.get(['user', 'name']),
        age: doc.get(['user', 'age']),
        langs: doc.get(['user', 'langs']),
        price: doc.getDecimal(['price']),
        text: doc.stringify(),
    })
    doc.free()
}

A headless Chromium runs the example and fails unless the Worker answered what the document holds and the transfer detached the buffer. How the transfer compares with postMessage of an object is measured in Benchmarks.

Documents shared between workers

The transfer above gives each Worker a copy of the image, and one Worker’s copy is its own. A second build of the module, js/shared/webcpp-document-shared.mjs, shares documents instead: its WebAssembly memory is one SharedArrayBuffer, and every Worker the program creates can join it. Several Workers then read the same document at once, in place: nothing is transferred and nothing is copied, and the image a Worker reads is at the address, in that memory, where the page parsed it. It is the shared_document of Sharing, taught from JavaScript.

Every document of this module is in the memory’s pool, whose blocks are the room of an image, and a Document is one reference to a document, as a std::shared_ptr is: useCount counts the references of every Worker, and the last one given up, by free() or by the collector, gives the block back. A write is every holder’s, since they read the same bytes. makeMutable() first makes the document the writer’s own: a copy in a block of its own when others hold it, the same block when none does, while the others go on reading the original (Copy on write).

The page starts the module when it imports the file, and hands a Worker two things in a message: connection(), which the Worker joins, and a token for the document. The Worker awaits join(connection), which runs the module on the shared memory in that Worker, and then adopts the token. The page, js/shared/example/page.mjs, parses a document and gives its token to four Workers:

Copied from js/shared/example/page.mjs
const free = pool.available
const doc = Document.parse('{"user":{"name":"Ada","langs":["C","Note G"]},"price":1.50}')
const address = doc.image().byteOffset
say(`page: parsed into one block of the pool, use count ${doc.useCount}`)

const workers = Array.from({ length: 4 }, start)
const adopted = await Promise.all(workers.map(worker =>
    worker.send({ command: 'adopt', connection: connection(), token: doc.share() })))

and a Worker, js/shared/example/reader.mjs, joins, adopts, and writes on a copy of its own:

Copied from js/shared/example/reader.mjs
const commands = {
    async adopt({ connection, token }) {
        await join(connection)
        doc = SharedDocument.adopt(token)
        return describe()
    },
    // The document made this worker's own, a copy in a block of its own, then changed.
    write({ name }) {
        doc.makeMutable()
        doc.set(['user', 'name'], name)
        return describe()
    },
    free() {
        doc.free()
        return {}
    },
}

A Worker that is done calls leave(): it drops every reference it holds, gives its slot back and may join again. A Worker that calls free() on a document drops that reference alone.

A token is what share() returns: a small object, { offset, ticket, generation }, that postMessage clones, and that keeps one reference to the document until a thread adopts it. SharedDocument.adopt(token) takes the reference over, once; SharedDocument.drop(token) gives it back without adopting it, as a receiver does that does not want the document. A token that was adopted or dropped is spent, and adopt and drop refuse it with a TypeError, even when its block holds another document by then, as does anything that is not a token:

Copied from js/shared/test/sharing.test.mjs
const doc = Document.parse(text)
const token = doc.share()
SharedDocument.drop(token)
assert.equal(doc.useCount, 1)
assert.throws(() => SharedDocument.adopt(token), TypeError)
assert.throws(() => SharedDocument.drop(token), TypeError)

A headless Chromium runs the example, and fails unless the page prints these lines, in which the four Workers read the page’s own address and the one that wrote does not:

Copied from js/shared/example/shared-workers.expected
cross-origin isolated: true
page: parsed into one block of the pool, use count 1
worker 1: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true
worker 2: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true
worker 3: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true
worker 4: reads Ada and C, Note G, at the page's address: true, in a SharedArrayBuffer: true
page: use count 5, blocks in use 1
worker 4: makeMutable, then set: reads Grace, at the page's address: false
page: reads Ada, use count 4, blocks in use 2
page: every worker freed its reference, use count 1
page: freed, every block back in the pool: true

A SharedArrayBuffer exists only in a page that is cross-origin isolated, which the server asks of the browser with two headers on the page and on every file it loads: Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. Without them the module does not start. Node needs neither.

The memory is laid out once, by the thread that starts the module, and has limits:

  • The memory is fixed at 128 MiB and does not grow, as growing a shared memory would make every Worker refresh its views of it. A full heap is an error, a RangeError, and not an abort.

  • The pool has 64 blocks of 512 KiB. A document is one block, so an image can be no larger than the room of a block: a change past it is a RangeError, and so is makeMutable() or Document.parse when no block is free. pool.available, pool.blocks and pool.blockBytes tell what is left.

  • At most 32 Workers are joined at once; a thirty-third join is a RangeError until one leaves. At most 1024 tokens are in flight at once.

  • A Worker that is terminated without calling leave() keeps its references and its slot for the life of the memory: its documents are not given back. A Worker that is done calls leave() first.

  • join.js, the Emscripten library that lets a Worker the program creates run the module, reads internals of Emscripten 6.0.11 (createWasm, wasmMemory, updateMemoryViews), so the module is tied to that version of Emscripten until it is checked against another.

Streams

StreamParser is the stream parser of Streams of documents, with the same framings, concatenated (the default), lines for NDJSON and rfc7464, and the same policy on errors, onError stop or skip, the latter for the framings that can go on. StreamParser.from(source, options) is an async generator that takes a ReadableStream of bytes or strings, such as a response body, or any async iterable of them, and yields a Document as soon as the bytes that end it arrive, whatever the chunks:

Copied from js/test/stream.test.mjs
for await (const doc of StreamParser.from(chunked(encoder.encode(lines), size), { framing: 'lines' })) {
    assert.ok(doc instanceof Document)
    out.push(doc.toJSON())
}

A bad line throws a SyntaxError once the documents before it are yielded, and under onError skip it is dropped and counted in parser.dropped. The end of the source ends the stream, as finish() does, so a top-level number or a last line without its newline is delivered (Errors).

The parser can also be driven by hand, which is how a sender written in JavaScript feeds the parser compiled from C++: write takes a chunk, read gives the next complete document or undefined, and finish ends the stream:

Copied from js/test/stream.test.mjs
const parser = new StreamParser({ framing: 'lines' })
parser.write(encoder.encode('{"a":'))
assert.equal(parser.read(), undefined)
parser.write(encoder.encode('1}\n'))
assert.deepEqual(parser.read().toJSON(), { a: 1 })
assert.equal(parser.read(), undefined)
parser.finish()

Each document read or from gives is a Document, in WebAssembly memory like any other, which the program frees when it has used it.

From JavaScript

document’s interface is JavaScript’s ways with JSON, written in C++: a name of JavaScript becomes a member function of the same meaning, and an operation that JavaScript makes throw returns its error instead. A program that knows how JavaScript reads and writes a parsed object knows how document does. This section is for the C++ programmer who knows JavaScript; the JavaScript programmer who wants document in a page, a Worker or Node reads Using it from JavaScript, and what C++ gives that JavaScript does not is in What C++ gives JavaScript.

JavaScript, then C++

Each row is a JavaScript expression and what a program of document writes for it, v being a handle (Handles) and doc a document:

JavaScript document

JSON.parse(text)

wd::parse(text), which returns a result; wd::parse_or_throw(text) throws, with exceptions only (Parsing)

JSON.stringify(v, null, 2)

v.stringify(2), wd::stringify(doc, 2), and wd::stringify_to(doc, sink, 2) in pieces (Writing JSON)

v.key, v["key"], v[3]

v["key"], v[3]: a handle, which never fails

v.at(-1)

v.at(-1)

typeof v, Array.isArray(v), v === null

v.kind(), v.is_array(), v.is_null(), and an is_ function for every kind

v === undefined

v.is_undefined()

v ?? fallback

v.value_or(fallback)

"key" in v

v.contains("key")

v.length, of an array or a string

v.length(), in UTF-16 code units for a string

Object.keys(v).length

v.size()

Number.isInteger(v)

v.is_integer()

v.key = x

v["key"].set(x); v["key"] = x throws, with exceptions only (The sugar that throws)

delete v.key

v.erase("key"), or v["key"].set(wd::undefined)

Object.assign(v, source)

v.assign_from(source)

Object.entries(v), Object.keys(v), Object.values(v)

v.entries(), v.keys(), v.values()

for (const x of a)

for (wd::const_value x : a)

structuredClone(doc)

doc.clone()

a.push(x), a.splice(1, 2) and the other array methods

the same names (Arrays)

{key: 1}, [1, 2]

wd::object{{"key", 1}}, wd::array{1, 2}

The program below prints each JavaScript expression beside what document gives for it, with this helper, which writes undefined where nothing is there, as JavaScript prints it:

/** Prints a JavaScript expression and the value it reads, `undefined` when nothing is there. */
void show(std::string_view expression, const wd::const_value& v) {
    std::cout << expression << " -> " << (v.is_undefined() ? "undefined" : v.stringify()) << '\n';
}

Handles walks through the program; every line it prints is what Node prints for the same JavaScript.

Where document differs, on purpose

A few things JavaScript does would make a document either unsafe or unlike JSON. document does otherwise there, and each difference keeps the text JSON.stringify writes, but where the table says it does not:

JavaScript document Why

a.x = b.y makes both name one object

a["x"].set(b["y"]) copies the subtree, and so does h = other with exceptions

A document is a tree, as JSON is: no value is shared, and no cycle exists.

a.splice(0, 1, a) inserts the array into itself, a cycle

it inserts a copy of the array as it was

The same: a tree has no cycle.

o.x = undefined keeps the key x, which JSON.stringify omits

erases x

Nothing stores undefined. The text is the same; Object.keys would differ.

[undefined] holds undefined, written [null]

stores null

The same text.

arrays may have holes

null fills them, and pop or shift of what was a hole returns null, where JavaScript returns undefined

Arrays are dense (Arrays); the text is the same.

arr["0"] is element 0, and obj[3] the member "3"

arr["0"] names nothing, nor does obj[3]: a key reaches a member, an index an element

A handle names a member or an element, never both.

numbers are doubles

exact with the default traits; JavaScript’s doubles with js_traits

What is stored is what was sent (Numbers).

JSON.parse of 40 digits rounds, and of 1e999 gives Infinity

the default traits refuse 40 digits with errc::range, and store 1e999 exactly, which a JavaScript peer reads as Infinity; js_traits round 40 digits and make 1e999 Infinity, written null, as JavaScript does

The default traits keep what was sent; js_traits read it as JavaScript does (What each refuses).

-0

0

JSON.stringify(-0) writes 0 too.

JSON.parse('"\ud800"') keeps the lone surrogate

refused with errc::syntax, with either encoding; set keeps one with utf16

Boost.JSON’s parser, which document uses, cannot deliver one (Lone surrogates).

a Uint8Array is written {"0":1,"1":2}

bytes are written as base64 by default

The traits’ bytes policy; javascript_bytes restores JavaScript’s text (Strings, dates and bytes).

prototypes, getters, symbols, toJSON, a replacer, a reviver, Map, Set, BigInt

none

Out of scope: a document holds JSON’s values, and dates and bytes.

The program shows four of them: an index written as a key, a key written as an index, -0, and the lone surrogate the parser refuses:

wd::result<wd::document> made = wd::parse(R"({"list":[10,20],"o":{"3":"three"},"z":-0})");
if (made.has_error()) {
    return false;
}
wd::document& doc = *made;
show("doc.list['0']", doc["list"]["0"]);  // JavaScript: 10, an index written as a key
show("doc.o[3]", doc["o"][3]);            // JavaScript: "three", a key written as an index
show("doc.z", doc["z"]);                  // -0 is 0, as JSON.stringify writes it
say(R"(JSON.parse('"\ud800"') refused)", wd::parse(R"("\ud800")").error() == wd::errc::syntax);
doc.list['0'] -> undefined
doc.o[3] -> undefined
doc.z -> 0
JSON.parse('"\ud800"') refused -> true

JSON.parse gives 10, "three" and 0 for the first three, and keeps the lone surrogate.

Handles

A document holds one value, and a program reaches the values inside it through handles: wd::value, which reads and writes, and wd::const_value, which reads. doc.root(), doc["key"], doc[3] and every navigation from a handle return one. A handle names one place in one document, and is cheap to copy: it holds the document’s address and the ids of nodes, not a pointer into the tree.

Navigation never fails

In JavaScript, doc.user.nick is undefined when user has no nick, and doc.n.x.y throws a TypeError when doc.n is a number. A handle never fails: it names one of three things, and reads undefined where nothing is there.

A handle names Made by Reads, and set does

a node

navigation to a value that exists

the value; set replaces it

a missing member or element of a container that exists

obj["absent"], or arr[9] past the end

undefined; set creates the member, or the element, filling the elements between with null

nothing

navigation through undefined, a scalar, or an erased value

undefined; set fails with errc::type, JavaScript’s TypeError

The program of From JavaScript parses a user and a number, and navigates:

constexpr std::string_view text = R"({"user":{"name":"Ada","tags":["math"]},"n":3})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
    return false;
}
wd::document& doc = *made;
wd::value user = doc["user"];

show("user.name", user["name"]);
show("user.nick", user["nick"]);        // a missing member: set creates it
show("doc.n.x.y", doc["n"]["x"]["y"]);  // nothing: JavaScript throws a TypeError here
show("user.tags[0]", user["tags"][0]);
show("user.tags.at(-1)", user["tags"].at(-1));
user.name -> "Ada"
user.nick -> undefined
doc.n.x.y -> undefined
user.tags[0] -> "math"
user.tags.at(-1) -> "math"

A handle resolves its place again at every call, so it reads what is there now: user["nick"] reads undefined until a set creates the member, and the same handle then reads the new value. A handle stays valid while the document grows and its image moves to a larger buffer; it reads undefined once its value is erased; and it is invalid once its document is moved from or destroyed, as a container’s iterators are.

Reading

kind() is JavaScript’s typeof, with null, arrays, dates and bytes told apart: one of the enumerators of wd::kind, undefined, null, boolean, number, string, date, bytes, array and object, each with an is_ function. get<T>() reads the value as T and returns a result: errc::type for another kind, and errc::range for a number that T cannot hold exactly. T is bool, any integer type, a Boost.Decimal type, the traits’ number type, std::string, std::string_view, std::u16string, std::u16string_view, wd::date_ms, std::vector<std::byte> or std::span<const std::byte> (Strings, dates and bytes).

value_or(fallback) is JavaScript’s ??: the value get<T>() gives, T being the fallback’s type, or the fallback when nothing is there, when the value is null, or when it cannot be read as T. contains(key) is key in v for an object, size() an object’s key count or an array’s length, and length() an array’s length or a string’s in UTF-16 code units, as JavaScript counts them:

say("typeof user.name === 'string'", user["name"].is_string());
say("'tags' in user", user.contains("tags"));
if (const wd::result<std::uint64_t> length = user["name"].length()) {
    say("user.name.length", *length);
}
say("doc.n ?? 0", doc["n"].value_or(std::int64_t{0}));
say("user.nick ?? 'none'", user["nick"].value_or(std::string_view("none")));
say("Number.isInteger(doc.n)", doc["n"].is_integer());
typeof user.name === 'string' -> true
'tags' in user -> true
user.name.length -> 3
doc.n ?? 0 -> 3
user.nick ?? 'none' -> none
Number.isInteger(doc.n) -> true

A std::string_view that get returns, and a key that keys() gives, are views into the document’s image: each is valid until the document’s next change, which may move the image.

Writing

set(x) is assignment. It takes bool, any integer type, the Boost.Decimal types, the traits’ number type, text (std::string_view, const char*, std::string, std::u16string_view), bytes (std::span<const std::byte>), a wd::date_ms, nullptr or wd::null, the literals wd::object and wd::array, which are JSON written in C++, a handle or a document of the same traits, which it copies, and wd::undefined. A character is not a number: set('a') does not compile, nor does set of a float, nor of a double unless the traits’ number type is one.

erase(key) is delete, and returns whether the key was there. set(wd::undefined) erases a member too: JavaScript keeps a member whose value is undefined, and writes nothing for it, while a document never stores undefined. In an array, it stores null, the text JavaScript writes for it; at the root, the document holds undefined, and writes nothing. assign_from(source) is Object.assign: each member of the source is set here, in its order, as one operation that completes or changes nothing; an array’s elements are its members "0" to "n-1", and null or undefined add nothing.

// user.nick = "Countess"; user.age = 36; user.tags.push("poetry"); user.home = {...};
if (!user["nick"].set("Countess") || !user["age"].set(36) || !user["tags"].push("poetry") ||
    !user["home"].set(wd::object{{"city", "London"}, {"rooms", wd::array{1, 2}}})) {
    return false;
}
show("user", user);

// delete user.age; user.nick = undefined, which erases it; Object.assign(user, {...}).
const wd::result<bool> erased = user.erase("age");
if (!erased || !user["nick"].set(wd::undefined)) {
    return false;
}
say("delete user.age", *erased);
wd::result<wd::document> defaults = wd::parse(R"({"lang":"en","name":"Lovelace"})");
if (defaults.has_error() || !user.assign_from(defaults->root())) {
    return false;
}
show("user", user);
user -> {"name":"Ada","tags":["math","poetry"],"nick":"Countess","age":36,"home":{"city":"London","rooms":[1,2]}}
delete user.age -> true
user -> {"name":"Lovelace","tags":["math","poetry"],"home":{"city":"London","rooms":[1,2]},"lang":"en"}

Every writing function returns a result and is [[nodiscard]]. It completes, or it changes nothing: it finds the room it needs in the image before it allocates (Images), so a full image, a value of the wrong kind or a number out of range leave the document as it was.

Copies, never aliases

JavaScript’s doc.copy = doc.user makes two names of one object, so that a change through one is seen through the other. A document is a tree, as JSON is, so set of a handle copies the subtree:

// doc.copy = doc.user shares one object in JavaScript; here it is a copy of the subtree.
if (!doc["copy"].set(user) || !doc["copy"]["name"].set("Byron")) {
    return false;
}
show("user.name", user["name"]);
show("copy.name", doc["copy"]["name"]);
user.name -> "Lovelace"
copy.name -> "Byron"

A copy reads its source before it changes anything, so a value may be copied into its own subtree. Between documents, copies need the same traits, and any storage. doc.clone() copies a whole document into a new owned one, as structuredClone would.

A wd::value is not a regular value: with exceptions, x = y writes where x points, and never makes it point elsewhere (The sugar that throws). It must not be swapped or sorted: std::swap(x, y) assigns through both handles, leaving both places holding the value of y, and std::sort over a range of handles writes the document’s values the same way. Read the values out, sort them, and set them back.

Iteration and equality

entries(), keys() and values() are Object.entries, Object.keys and Object.values, in the order of the traits’ object policy (Key order), and a for over an array’s handle visits its elements. A key is a std::string_view with UTF-8 strings, and a std::u16string_view with UTF-16 ones (Traits):

// for (const [key, value] of Object.entries(user))
for (auto [key, value] : user.entries()) {
    std::cout << key << ": " << value.stringify() << '\n';
}
// for (const tag of user.tags)
for (const wd::const_value tag : user["tags"]) {
    std::cout << "tag " << tag.stringify() << '\n';
}
name: "Lovelace"
tags: ["math","poetry"]
home: {"city":"London","rooms":[1,2]}
lang: "en"
tag "math"
tag "poetry"

A range reads the document as it is when each step is taken, so changing the container it walks is undefined, as it is for a standard container.

== compares two values structurally, which JavaScript does not: the same kind; numbers by value, so 1, 1.0 and 1e0 are equal; strings, dates and bytes by value; arrays element by element; objects with the same keys, each holding an equal value, in any order. Two documents compare their roots.

Numbers

JavaScript’s numbers are doubles: JSON.parse rounds every number to the nearest one, so a peer that sends 34 exact digits receives 17. With its default traits, document stores every number exactly as it was sent, and writes it back as JavaScript would write that value; with js_traits, it holds JavaScript’s own doubles, and agrees with JavaScript on every digit.

Stored by value

A number is stored in one of two forms, chosen by its value, never by its spelling: an integer that fits the traits’ integer, std::int64_t, is one; any other number is the traits’ number, Boost.Decimal’s decimal128_t by default, normalised, without trailing zeros. So 1, 1.0 and 1e0 are one stored value, as they are one number in JavaScript, and 18446744073709551615, past std::int64_t, is an exact number. Parsing reads a number from its text, never through a double: more than 34 significant digits, or an exponent past the range of decimal128_t, is errc::range.

Writing follows JavaScript’s Number::toString on the stored digits, with k significant digits and the decimal point after position n:

When Written as Example

k <= n <= 21

the digits, then n - k zeros

1e20 writes 100000000000000000000

0 < n <= 21

the digits, with a point after n of them

123.45

-6 < n <= 0

0., then -n zeros, then the digits

0.000001

otherwise

one digit, a point and the others if any, e, the sign, n - 1

1e+21, 1e-7, 1.5e+300

So every number JSON.stringify writes, the shortest text of a double, is written back byte for byte as JavaScript writes it, and a number a double cannot hold stays exact. The program parses one text with each traits:

constexpr std::string_view text =
    "[0.1000000000000000000000000000000001,18446744073709551615,4611686018427387904,"
    "1e21,1.50,1E-7,1.0,-0]";
parsed("default_traits", wd::parse(text));
parsed("js_traits     ", wd::parse<wd::js_traits>(text));
default_traits: [0.1000000000000000000000000000000001,18446744073709551615,4611686018427387904,1e+21,1.5,1e-7,1,0]
js_traits     : [0.1,18446744073709552000,4611686018427388000,1e+21,1.5,1e-7,1,0]

With js_traits, the numbers are what Node holds, written as Node writes them: the 34 digits round to 0.1, and 262, 4611686018427387904, is written 4611686018427388000. A number under js_traits is first rounded to a double, then stored as an integer when that double is integral and fits std::int64_t, so it writes back as Node writes the double.

What each refuses

The default traits refuse a number they cannot store exactly: more than 34 significant digits, or an exponent past the range of decimal128_t, beyond about 1e6144. Within it they store every number exactly, 1e999 included, which a JavaScript peer reads as Infinity. js_traits refuse no number: they round as JSON.parse does, and a number past a double’s range is Infinity, as JSON.parse makes it, which stringify writes null, as JSON.stringify does:

constexpr std::string_view forty_digits = "1234567890123456789012345678901234567890";
parsed("40 digits, default_traits", wd::parse(forty_digits));
parsed("40 digits, js_traits     ", wd::parse<wd::js_traits>(forty_digits));
parsed("1e999, default_traits    ", wd::parse("1e999"));
parsed("1e999, js_traits         ", wd::parse<wd::js_traits>("1e999"));
40 digits, default_traits: a number or index out of range
40 digits, js_traits     : 1.2345678901234568e+39
1e999, default_traits    : 1e+999
1e999, js_traits         : null

set refuses a NaN or an infinity of a Boost.Decimal type with errc::range. A double’s NaN and infinities, which arithmetic makes, and a parse past a double’s range, are stored under js_traits, and written null, as JavaScript writes them.

Reading a number

get<T>() gives a number as any integer type, a Boost.Decimal type, or the traits’ number type, when T holds it exactly, and errc::range otherwise: a number that is not an integer, or does not fit, is never truncated or rounded into T. is_integer() is Number.isInteger, true for a number such as 1e30 too:

wd::result<wd::document> made =
    wd::parse(R"({"small":300,"big":18446744073709551615,"half":0.5,"huge":1e30})");
if (made.has_error()) {
    return 1;
}
const wd::document& doc = *made;
read("small as int64_t", doc["small"].get<std::int64_t>());
read("small as int8_t", doc["small"].get<std::int8_t>());
read("big as uint64_t", doc["big"].get<std::uint64_t>());
read("big as int64_t", doc["big"].get<std::int64_t>());
read("half as int64_t", doc["half"].get<std::int64_t>());
std::cout << "Number.isInteger(huge): " << std::boolalpha << doc["huge"].is_integer() << '\n';
small as int64_t: 300
small as int8_t: a number or index out of range
big as uint64_t: 18446744073709551615
big as int64_t: a number or index out of range
half as int64_t: a number or index out of range
Number.isInteger(huge): true

A double is read or set only when the traits’ number type is one: the default traits have no conversion from or to double, nor to float ever, so a program cannot lose digits by accident.

Arithmetic

A document stores numbers; it computes nothing. A program reads a value in the traits’ own number type, computes, and stores the result, so the arithmetic is the number type’s: exact in decimal, binary in a double.

// 0.1 + 0.2, computed by the program in the traits' own number type, then stored.
wd::result<wd::document> exact = wd::parse(R"({"a":0.1,"b":0.2})");
wd::result<wd::basic_document<wd::js_traits>> js =
    wd::parse<wd::js_traits>(R"({"a":0.1,"b":0.2})");
if (exact.has_error() || js.has_error()) {
    return 1;
}
using decimal = boost::decimal::decimal128_t;
const wd::result<decimal> a = (*exact)["a"].get<decimal>();
const wd::result<decimal> b = (*exact)["b"].get<decimal>();
const wd::result<double> x = (*js)["a"].get<double>();
const wd::result<double> y = (*js)["b"].get<double>();
if (!a || !b || !x || !y || !(*exact)["sum"].set(*a + *b) || !(*js)["sum"].set(*x + *y)) {
    return 1;
}
std::cout << exact->stringify() << '\n' << js->stringify() << '\n';
{"a":0.1,"b":0.2,"sum":0.3}
{"a":0.1,"b":0.2,"sum":0.30000000000000004}

Strings, dates and bytes

JSON has strings; JavaScript programs also send dates and bytes through it, as text. A document stores strings in the encoding its traits choose, and holds dates and bytes as values of their own, which it writes as JSON.stringify writes a Date and, by its traits, a Uint8Array.

Strings

The traits’ string is the stored encoding of strings and keys: wd::utf8, the default, or wd::utf16, the code units JavaScript holds, which js_traits choose. Either way, length() counts UTF-16 code units, as JavaScript does: UTF-8 is decoded to count them. Text is validated where it enters: the parser refuses invalid UTF-8, and set refuses text invalid in its encoding with errc::type.

A view reads a string as it is stored, and cannot transcode: get<std::string_view>() reads UTF-8 strings only, and get<std::u16string_view>() UTF-16 ones; get<std::string>() and get<std::u16string>() transcode from either. Keys come out as the encoding’s view. The sugar’s as_string() reads UTF-8 strings only: under js_traits, a program uses as<std::u16string_view>() or as<std::string>() (The sugar that throws).

constexpr std::string_view text = R"(["é😀","tab\there \"q\" \u0001"])";
wd::result<wd::document> utf8 = wd::parse(text);
wd::result<wd::basic_document<wd::js_traits>> utf16 = wd::parse<wd::js_traits>(text);
if (utf8.has_error() || utf16.has_error()) {
    return false;
}
// "é😀".length is 3 in UTF-16 code units, whatever the stored encoding.
read("UTF-8 length ", (*utf8)[0].length());
read("UTF-16 length", (*utf16)[0].length());
read("UTF-8 as std::string_view", (*utf8)[0].get<std::string_view>());
read("UTF-8 as std::u16string_view", (*utf8)[0].get<std::u16string_view>());
read("UTF-16 as std::u16string_view", (*utf16)[0].get<std::u16string_view>());
read("UTF-16 as std::string", (*utf16)[0].get<std::string>());
std::cout << "both write: " << utf8->stringify() << ' ' << utf16->stringify() << '\n';

// A lone surrogate: kept by UTF-16 and written as JSON.stringify writes it; refused by UTF-8.
constexpr std::array<char16_t, 1> lone{0xD800};
const std::u16string_view surrogate(lone.data(), lone.size());
print("UTF-16 set", (*utf16)[2].set(surrogate));
print("UTF-8 set ", (*utf8)[2].set(surrogate));
std::cout << "UTF-16 writes: " << (*utf16)[2].stringify() << '\n';
UTF-8 length : 3
UTF-16 length: 3
UTF-8 as std::string_view: é😀
UTF-8 as std::u16string_view: a value of another kind
UTF-16 as std::u16string_view: 3 code units
UTF-16 as std::string: é😀
both write: ["é😀","tab\there \"q\" \u0001"] ["é😀","tab\there \"q\" \u0001"]
UTF-16 set: ok
UTF-8 set : a value of another kind
UTF-16 writes: "\ud800"

Both encodings write the same text, escaped as JSON.stringify escapes it: a quotation mark and a backslash with a backslash; \b, \f, \n, \r and \t; any other character below U+0020 as \u and four lowercase hexadecimal digits; nothing else, so /, text beyond ASCII and U+2028 and U+2029 are written as they are.

A lone surrogate is a UTF-16 code unit of a pair whose other half is missing, which JavaScript’s strings may hold. UTF-8 cannot encode one, so set refuses it with wd::utf8; wd::utf16 keeps it, and writes it as \u and its four digits, as JSON.stringify has since ES2019. The parser refuses one with either encoding (Lone surrogates).

Dates

A date is a wd::date_ms, milliseconds since the epoch, 1970-01-01T00:00:00Z, as a JavaScript Date holds it, and its range is JavaScript’s, 8.64e15 ms on either side: set past it is errc::range. It is written as Date.prototype.toJSON writes it, "2026-10-12T03:04:05.678Z", with a year outside 0 to 9999 written as six digits and a sign, "+275760-09-13T00:00:00.000Z".

JSON has no dates, so a parse keeps every string a string, as JSON.parse does, unless parse_options::revive_dates, which turns a string in exactly that format into a date. get<wd::date_ms>() reads such a string as a date too, revived or not:

constexpr std::string_view text = R"({"at":"2026-10-12T03:04:05.678Z","day":"2026-10-12"})";
wd::parse_options revive;
revive.revive_dates = true;
wd::result<wd::document> plain = wd::parse(text);
wd::result<wd::document> revived = wd::parse(text, revive);
if (plain.has_error() || revived.has_error()) {
    return false;
}
std::cout << "is_date: " << std::boolalpha << (*plain)["at"].is_date() << ' '
          << (*revived)["at"].is_date() << ' ' << (*revived)["day"].is_date() << '\n';
// A string in Date.prototype.toJSON's format reads as a date, revived or not.
const wd::result<wd::date_ms> at = (*plain)["at"].get<wd::date_ms>();
std::cout << "ms since the epoch: " << (at ? at->time_since_epoch().count() : 0) << '\n';

const wd::date_ms last{std::chrono::milliseconds{8'640'000'000'000'000}};
print("set the last date", (*plain)["last"].set(last));
print("set one ms later ", (*plain)["later"].set(last + std::chrono::milliseconds{1}));
print("written", plain);
is_date: false true false
ms since the epoch: 1791774245678
set the last date: ok
set one ms later : a number or index out of range
written: {"at":"2026-10-12T03:04:05.678Z","day":"2026-10-12","last":"+275760-09-13T00:00:00.000Z"}

Bytes

Bytes are a std::span<const std::byte> that set stores raw in the image, and that an image carries as they are. stringify writes them as the traits’ bytes policy says:

Policy Written as

wd::base64, the default

base64 of RFC 4648, with padding

wd::base64url

base64url of RFC 4648, section 5, without padding

wd::javascript_bytes, in js_traits

what JSON.stringify writes for a Uint8Array: an object of the indices and the bytes

parse_options::revive_bytes turns a string valid in the policy’s text back into bytes, and, with javascript_bytes, an object whose keys are exactly "0" to "n-1" holding integers from 0 to 255. get<std::vector<std::byte>>() reads such a value as bytes too, and get<std::span<const std::byte>>() reads bytes in place:

const std::array<std::byte, 4> raw{
    std::byte{0xde},
    std::byte{0xad},
    std::byte{0xbe},
    std::byte{0xef},
};
const std::span<const std::byte> view(raw);
wd::result<wd::document> base64 = wd::document::create();
wd::result<wd::basic_document<canonical_traits>> base64url =
    wd::basic_document<canonical_traits>::create();
wd::result<wd::basic_document<wd::js_traits>> js = wd::basic_document<wd::js_traits>::create();
if (base64.has_error() || base64url.has_error() || js.has_error() ||
    !base64->root().set(view) || !base64url->root().set(view) || !js->root().set(view)) {
    return false;
}
print("base64          ", base64);
print("base64url       ", base64url);
print("javascript_bytes", js);

// revive_bytes turns text of the bytes policy back into bytes.
wd::parse_options revive;
revive.revive_bytes = true;
wd::result<wd::document> revived = wd::parse(R"({"key":"3q2+7w=="})", revive);
if (revived.has_error()) {
    return false;
}
const wd::result<std::vector<std::byte>> key = (*revived)["key"].get<std::vector<std::byte>>();
std::cout << "revived: " << std::boolalpha << (*revived)["key"].is_bytes() << ", "
          << (key ? key->size() : 0) << " bytes\n";
base64          : "3q2+7w=="
base64url       : "3q2-7w"
javascript_bytes: {"0":222,"1":173,"2":190,"3":239}
revived: true, 4 bytes

The program’s canonical_traits choose base64url (Traits).

Key order

The order of an object’s keys is what makes two JSON.stringify texts equal or not, and JavaScript’s is not the order of insertion. Its OrdinaryOwnPropertyKeys gives the keys that are array indices first, ascending, then every other key in the order it was created. Measured in Node 26, JSON.parse('{"b":1,"2":2,"a":3,"1":4}') holds {"1":4,"2":2,"b":1,"a":3}, and a document writes the same.

Array indices

An array index is a key that is the canonical decimal spelling of an integer from 0 to 4,294,967,294: "0", "7" and "10" are, and "01", "-1", "1.0", " 1" and "4294967295" are not, so they keep their place among the other keys. Assigning an existing key keeps its place. Erasing a key and creating it again puts it where a new key goes: last among the other keys, or back among the indices, ascending, for an index.

The policies

The traits’ object chooses the order:

Policy The order of an object’s keys

wd::javascript_order, the default

JavaScript’s: array indices ascending, then the other keys in the order they were created

wd::insertion_order

the order each key was created, the text’s order for a parsed object, whatever the key

wd::sorted

by the keys’ UTF-16 code units, with either encoding, as JavaScript’s sort() and RFC 8785 order them: for canonical JSON, signatures and hashes

A repeated key in parsed text is accepted as JSON.parse accepts it: the last value, in the first one’s place. The program parses one text under each policy, the last two through traits of its own (Traits), then erases and creates again a key and an index:

constexpr std::string_view text = R"({"b":1,"2":2,"a":3,"1":4,"01":5})";
print("javascript_order", wd::parse(text));
print("insertion_order ", wd::parse<insertion_traits>(text));
print("sorted          ", wd::parse<canonical_traits>(text));

// An index key erased and created again goes back among the indices; any other key, last.
wd::result<wd::document> made = wd::parse(text);
if (made.has_error() || !made->root().erase("b") || !(*made)["b"].set(6) ||
    !made->root().erase("1") || !(*made)["1"].set(7)) {
    return false;
}
print("erased, created ", made);
javascript_order: {"1":4,"2":2,"b":1,"a":3,"01":5}
insertion_order : {"b":1,"2":2,"a":3,"1":4,"01":5}
sorted          : {"01":5,"1":4,"2":2,"a":3,"b":1}
erased, created : {"1":7,"2":2,"a":3,"01":5,"b":6}

Every operation keeps the order: set, erase, assign_from, a copy and the parser. An object’s members are kept in ordered indices, never in a hashed one, since keys come from untrusted text: no input makes a lookup slower than its logarithm.

Arrays

A handle of an array has JavaScript’s array methods, under their JavaScript names, with JavaScript’s index rules. Each is a core function: it returns a result, completes or changes nothing, and fails with errc::type, JavaScript’s TypeError, on anything but an array.

JavaScript document Returns

a.push(x, y)

a.push(x, y)

result<std::uint64_t>: the new length

a.pop()

a.pop()

result<document>: a new document holding the removed element, or holding undefined when the array was empty

a.shift()

a.shift()

the same, from the front

a.unshift(x, y)

a.unshift(x, y)

result<std::uint64_t>: the new length

a.splice(start, count, x, y)

a.splice(start, count, x, y)

result<document>: the array of the removed elements

a.splice(start)

a.splice(start)

the same, removing every element from start

a.reverse()

a.reverse()

result<void>

a.slice(start, end)

a.slice(start, end)

result<document>: a new array of copies of the elements

a.concat(b, x)

a.concat(b, x)

result<document>: a new array of this array’s elements, then each argument’s, an array’s elements or the value itself

a.at(i)

a.at(i)

a handle, which names nothing out of range

a.length = n

a.set_length(n)

result<void>: truncates, or appends null

a[i] = x past the end

a[i].set(x)

result<void>: the elements between become null

The values a method takes are what set takes (Writing), each copied into the array; a method that returns removed or copied elements returns them in a new owned document, so that they outlive the array. The program logs each call, what it returned and the array after it:

/** The text of a value, `undefined` when nothing is there. */
std::string text(const wd::const_value& v) {
    return v.is_undefined() ? "undefined" : v.stringify();
}

/** Prints a call, what it returned and the array after it. */
void log(std::string_view call, std::string_view returned, const wd::const_value& a) {
    std::cout << call << " -> " << returned << ", a = " << text(a) << '\n';
}

/** Prints a call that returns a document: the removed values, or a new array. */
void log(std::string_view call, const wd::result<wd::document>& r, const wd::const_value& a) {
    log(call, r.has_value() ? text(r->root()) : r.error().message(), a);
}

/** Prints a call that returns a new length. */
void log(std::string_view call, const wd::result<std::uint64_t>& r, const wd::const_value& a) {
    log(call, r.has_value() ? std::to_string(*r) : r.error().message(), a);
}

/** Prints a call that returns nothing. */
void log(std::string_view call, const wd::result<void>& r, const wd::const_value& a) {
    log(call, r.has_value() ? "undefined" : r.error().message(), a);
}
wd::value a = made->root();
log("a.push(6, 7)", a.push(6, 7), a);
log("a.pop()", a.pop(), a);
log("a.shift()", a.shift(), a);
log("a.unshift(0)", a.unshift(0), a);
log("a.splice(1, 2, 'x', 'y', 'z')", a.splice(1, 2, "x", "y", "z"), a);
log("a.reverse()", a.reverse(), a);
log("a.slice(-2)", a.slice(-2), a);
log("a.concat([8, 9], 10)", a.concat(wd::array{8, 9}, 10), a);
log("a.at(-1)", text(a.at(-1)), a);
a.push(6, 7) -> 7, a = [1,2,3,4,5,6,7]
a.pop() -> 7, a = [1,2,3,4,5,6]
a.shift() -> 1, a = [2,3,4,5,6]
a.unshift(0) -> 6, a = [0,2,3,4,5,6]
a.splice(1, 2, 'x', 'y', 'z') -> [2,3], a = [0,"x","y","z",4,5,6]
a.reverse() -> undefined, a = [6,5,4,"z","y","x",0]
a.slice(-2) -> ["x",0], a = [6,5,4,"z","y","x",0]
a.concat([8, 9], 10) -> [6,5,4,"z","y","x",0,8,9,10], a = [6,5,4,"z","y","x",0]
a.at(-1) -> 0, a = [6,5,4,"z","y","x",0]

Indices

Indices follow JavaScript exactly. A negative start counts from the end and is clamped at 0, and a start past the length is the length; an omitted count removes to the end, and a negative one is 0. The end of slice follows the same rules as start, and at counts a negative index from the end:

if (!a.set(wd::array{1, 2, 3, 4, 5})) {
    return 1;
}
log("a.splice(-2, 1)", a.splice(-2, 1), a);
log("a.splice(10, 0, 'end')", a.splice(10, 0, "end"), a);
log("a.splice(1, -5)", a.splice(1, -5), a);
log("a.splice(3)", a.splice(3), a);
a.splice(-2, 1) -> [4], a = [1,2,3,5]
a.splice(10, 0, 'end') -> [], a = [1,2,3,5,"end"]
a.splice(1, -5) -> [], a = [1,2,3,5,"end"]
a.splice(3) -> [5,"end"], a = [1,2,3]

Dense arrays

JavaScript’s arrays may have holes, elements that were never set; JSON has none, and writes a hole as null. A document’s arrays are dense, the traits’ only array policy, wd::dense: a longer length, or a write past the end, fills the elements between with null, which writes the same text. So pop or shift of an element that was a hole returns null, where JavaScript returns undefined:

log("a.length = 1", a.set_length(1), a);
log("a.length = 3", a.set_length(3), a);
log("a[5] = 6", a[5].set(6), a);
log("a.pop()", a.pop(), a);
log("a.pop()", a.pop(), a);  // JavaScript: undefined, the element of a hole
a.length = 1 -> undefined, a = [1]
a.length = 3 -> undefined, a = [1,null,null]
a[5] = 6 -> undefined, a = [1,null,null,null,null,6]
a.pop() -> 6, a = [1,null,null,null,null]
a.pop() -> null, a = [1,null,null,null]

Copies and empty arrays

pop and shift of an empty array return a document that holds undefined, as JavaScript returns undefined. And since a document is a tree, a.splice(0, 1, a) inserts a copy of the array as it was before the call, where JavaScript inserts the array itself, a cycle; the copy is a value of its own:

if (!a.set(wd::array{})) {
    return 1;
}
log("a.pop()", a.pop(), a);  // a document that holds undefined
// JavaScript inserts the array itself, a cycle; a document inserts a copy of it.
if (!a.set(wd::array{1, 2})) {
    return 1;
}
log("a.splice(0, 1, a)", a.splice(0, 1, a), a);
log("a[0].push({})", a[0].push(wd::object{}), a);  // the copy, not a
a.pop() -> undefined, a = []
a.splice(0, 1, a) -> [1], a = [[1,2],2]
a[0].push({}) -> 3, a = [[1,2,{}],2]

A method that inserts many values, push, unshift, splice or concat, finds the room for all of them first: in a full external buffer, it returns errc::capacity and the array is as it was (Images).

Containers

The handles of Handles read and write a document as JavaScript does. A C++ program that walks a document also wants the interface its containers have, and document gives it: as_object() makes of an object a view that is a sorted map, as_array() makes of an array a view that is a vector, and both are ranges that the standard algorithms and views read. They are views of the document and own nothing, wd::object_ref and wd::array_ref, and every operation that can fail returns its result as the rest of the library does. The program of this section is example/document_containers.cpp, and prints the same lines on every target.

Its helpers print the message of a failed result and the document after an operation:

/** Prints the message of a failed result, and yields the exit code of main. */
template <class Result>
int fail(const Result& r) {
    std::cout << "error: " << r.error().message() << '\n';
    return 1;
}

/** Prints an operation and the document after it, as JSON. */
void step(std::string_view what, const wd::document& doc) {
    std::cout << what << " -> " << doc.stringify() << '\n';
}

An object is a sorted map

insert and try_emplace answer the member and whether it was inserted, and a key that is there keeps its value; insert_or_assign assigns to it, as JavaScript’s o[key] = v does. find answers the member’s iterator, or end(), contains whether the key is there, and erase how many members it removed. Iteration follows JavaScript’s order, so a new key comes last, and so does a key that is erased and inserted again (Key order):

// An object is a sorted map: insert and try_emplace answer the member and whether it was
// inserted, and insert_or_assign assigns to it as JavaScript's o[key] = v does.
wd::result<wd::document> made = wd::parse("{}");
if (made.has_error()) {
    return fail(made);
}
wd::document& doc = *made;
wd::result<wd::object_ref> as_object = doc.root().as_object();
if (as_object.has_error()) {
    return fail(as_object);
}
const wd::object_ref obj = *as_object;

const auto name = obj.insert("name", "Ada");
if (name.has_error()) {
    return fail(name);
}
std::cout << "insert name: " << name->second << '\n';

// A key that is there keeps its value.
const auto again = obj.insert("name", "Grace");
if (again.has_error()) {
    return fail(again);
}
std::cout << "insert name again: " << again->second
          << ", name = " << doc["name"].value_or(std::string_view("nobody")) << '\n';

const auto assigned = obj.insert_or_assign("name", "Augusta Ada");
if (assigned.has_error()) {
    return fail(assigned);
}
std::cout << "insert_or_assign name: " << assigned->second
          << ", name = " << doc["name"].value_or(std::string_view("nobody")) << '\n';

const auto born = obj.try_emplace("born", 1815);
if (born.has_error()) {
    return fail(born);
}
std::cout << "try_emplace born: " << born->second << '\n';

// find answers the member's iterator, or the end; contains answers whether the key is there.
const wd::object_ref::iterator found = obj.find("born");
std::cout << "find born: " << found->second.value_or(std::int64_t{0}) << '\n';
const wd::object_ref::iterator missing = obj.find("nope");
std::cout << "contains born: " << obj.contains("born")
          << ", find nope is end: " << (missing == obj.end()) << '\n';

const auto erased = obj.erase("born");
if (erased.has_error()) {
    return fail(erased);
}
std::cout << "erase born: " << *erased << ", contains born: " << obj.contains("born") << '\n';

// A new key comes last, and so does a key erased and inserted again: the members iterate in
// JavaScript's order, which is the order they were added in.
const auto langs = obj.insert("langs", wd::array{"Analytical", "Note G"});
if (langs.has_error()) {
    return fail(langs);
}
const auto known = obj.insert("known", true);
if (known.has_error()) {
    return fail(known);
}
const auto born_again = obj.insert("born", 1815);
if (born_again.has_error()) {
    return fail(born_again);
}
for (const auto& [key, handle] : obj) {
    std::cout << key << " = " << handle.stringify() << '\n';
}
insert name: true
insert name again: false, name = Ada
insert_or_assign name: false, name = Augusta Ada
try_emplace born: true
find born: 1815
contains born: true, find nope is end: true
erase born: 1, contains born: false
name = "Augusta Ada"
langs = ["Analytical","Note G"]
known = true
born = 1815

An array is a vector

push_back, emplace_back, insert, erase and resize change an array as they change a std::vector, and at checks the index: an index past the end is an error value and not an undefined element. erase answers the iterator of the element after the one it removed, and resize to a larger size fills with null (Dense arrays):

// An array is a vector: push_back, emplace_back, insert, erase and resize change it as
// std::vector does, and at checks the index.
wd::result<wd::document> list_made = wd::parse("[1,2,3]");
if (list_made.has_error()) {
    return fail(list_made);
}
wd::document& list_doc = *list_made;
wd::result<wd::array_ref> as_array = list_doc.root().as_array();
if (as_array.has_error()) {
    return fail(as_array);
}
const wd::array_ref list = *as_array;

const auto pushed = list.push_back(4);
if (pushed.has_error()) {
    return fail(pushed);
}
step("push_back(4)", list_doc);

const auto emplaced = list.emplace_back(5);
if (emplaced.has_error()) {
    return fail(emplaced);
}
step("emplace_back(5)", list_doc);

const auto placed = list.insert(std::ranges::next(list.begin(), 1), 9);
if (placed.has_error()) {
    return fail(placed);
}
step("insert(begin + 1, 9)", list_doc);

// erase answers the iterator of the element after the one it removed.
const wd::array_ref::iterator after = list.erase(std::ranges::next(list.begin(), 1));
step("erase(begin + 1)", list_doc);
const wd::value next_element = *after;
std::cout << "erase returns the element after: " << next_element.value_or(std::int64_t{0})
          << '\n';

const auto shrunk = list.resize(3);
if (shrunk.has_error()) {
    return fail(shrunk);
}
step("resize(3)", list_doc);
std::cout << "front " << list.front().value_or(std::int64_t{0}) << ", back "
          << list.back().value_or(std::int64_t{0}) << '\n';

const wd::result<wd::value> third = list.at(2);
if (third.has_error()) {
    return fail(third);
}
std::cout << "at(2) = " << third->value_or(std::int64_t{0}) << '\n';
// An index past the end is an error value, not an undefined element.
const wd::result<wd::value> beyond = list.at(10);
std::cout << "at(10) is errc::range: "
          << (beyond.has_error() && beyond.error() == wd::errc::range) << '\n';

const auto grown = list.resize(5);
if (grown.has_error()) {
    return fail(grown);
}
step("resize(5)", list_doc);
push_back(4) -> [1,2,3,4]
emplace_back(5) -> [1,2,3,4,5]
insert(begin + 1, 9) -> [1,9,2,3,4,5]
erase(begin + 1) -> [1,2,3,4,5]
erase returns the element after: 2
resize(3) -> [1,2,3]
front 1, back 3
at(2) = 3
at(10) is errc::range: true
resize(5) -> [1,2,3,null,null]

Ranges

An object and an array are each a std::ranges::bidirectional_range, so the standard algorithms and the views over them read their members as they read a vector or a map:

// An object and an array are ranges: the standard algorithms and the views over them read
// their members, as they read a vector.
wd::result<wd::document> shop_made = wd::document::create();
if (shop_made.has_error()) {
    return fail(shop_made);
}
wd::document& shop = *shop_made;
const auto shop_set = shop.root().set(wd::object{
    {"prices", wd::array{3, 8, 12, 5, 20}},
    {"stock", wd::object{{"apple", 4}, {"pear", 0}, {"plum", 7}}},
});
if (shop_set.has_error()) {
    return fail(shop_set);
}
wd::result<wd::array_ref> as_prices = shop["prices"].as_array();
if (as_prices.has_error()) {
    return fail(as_prices);
}
const wd::array_ref prices = *as_prices;
wd::result<wd::object_ref> as_stock = shop["stock"].as_object();
if (as_stock.has_error()) {
    return fail(as_stock);
}
const wd::object_ref stock = *as_stock;

const auto above_five = [](const wd::value& v) { return v.value_or(std::int64_t{0}) > 5; };
const auto doubled_value = [](const wd::value& v) { return 2 * v.value_or(std::int64_t{0}); };
std::cout << "prices above 5: " << std::ranges::count_if(prices, above_five) << '\n';
std::cout << "prices above 5, doubled:";
// The view is built and iterated in one statement: a filter view has no const iteration.
for (const std::int64_t twice :
     prices | std::views::filter(above_five) | std::views::transform(doubled_value)) {
    std::cout << ' ' << twice;
}
std::cout << '\n';

const auto empty = std::ranges::find_if(
    stock, [](const auto& member) { return member.second.value_or(std::int64_t{0}) == 0; });
std::cout << "first out of stock: " << empty->first << '\n';
prices above 5: 3
prices above 5, doubled: 16 24 40
first out of stock: pear

What a change leaves valid is more than the standard containers leave: an insertion, and any growth of the image, keeps every iterator and handle valid and invalidates only the views (std::string_view) read from a value; an erase invalidates the iterators to the erased member and its subtree. A range reads the document as it is when each step is taken, so changing the container it walks is undefined, as it is for a standard container.

Keys in order

An object is kept in JavaScript’s order, and also in key order: by_key() is the same object ordered by key, by UTF-16 code units whatever the encoding, with find, lower_bound, upper_bound and equal_range. Each step and each bound is a search of a tree, so a query costs O(log n), and nothing is sorted: a JavaScript program sorts every key to get any of these answers, and gets it again each time. A prefix query is a range from the first key at or after the prefix up to the first key at or after the prefix with its last character raised by one:

// by_key is the same object in key order, by UTF-16 code units, with bounds for ranges of keys.
// A JavaScript program sorts every key to get any of these; here the tree keeps them sorted.
wd::result<wd::document> catalog_made = wd::parse("{}");
if (catalog_made.has_error()) {
    return fail(catalog_made);
}
wd::document& catalog = *catalog_made;
wd::result<wd::object_ref> as_catalog = catalog.root().as_object();
if (as_catalog.has_error()) {
    return fail(as_catalog);
}
const wd::object_ref catalog_obj = *as_catalog;
const std::array<std::string_view, 13> names{
    "user_3", "admin",  "user_1", "guest",  "user_9", "user",   "user_2",
    "owner",  "user_7", "user_5", "user_4", "user_8", "user_6",
};
for (const std::string_view key : names) {
    const auto added = catalog_obj.insert(key, true);
    if (added.has_error()) {
        return fail(added);
    }
}
std::cout << "JavaScript order:";
for (const auto& member : catalog_obj) {
    std::cout << ' ' << member.first;
}
std::cout << '\n';
const auto keys = catalog_obj.by_key();
std::cout << "by_key order:";
for (const auto& member : keys) {
    std::cout << ' ' << member.first;
}
std::cout << '\n';

// The keys with the prefix "user_" lie from its first key up to the first key at or after
// "user`", the character after '_'.
const std::ranges::subrange users(keys.lower_bound("user_"), keys.lower_bound("user`"));
std::cout << "prefix user_: " << std::ranges::distance(users) << " keys, from "
          << users.begin()->first << " to " << std::ranges::prev(users.end())->first << '\n';

const auto [exact, exact_end] = keys.equal_range("user_5");
std::cout << "equal_range user_5: " << std::ranges::distance(exact, exact_end) << " member, "
          << exact->first << '\n';

std::cout << "lower_bound p: " << keys.lower_bound("p")->first << '\n';
const std::ranges::subrange between(keys.lower_bound("g"), keys.lower_bound("v"));
std::cout << "keys from g up to v: " << std::ranges::distance(between) << '\n';
JavaScript order: user_3 admin user_1 guest user_9 user user_2 owner user_7 user_5 user_4 user_8 user_6
by_key order: admin guest owner user user_1 user_2 user_3 user_4 user_5 user_6 user_7 user_8 user_9
prefix user_: 9 keys, from user_1 to user_9
equal_range user_5: 1 member, user_5
lower_bound p: user
keys from g up to v: 12

The module of Using it from JavaScript gives the same query to JavaScript as keysInRange.

Reserving room

A batch of insertions of a known size is made with the image reserved once, as a vector is reserved: the insertions then move nothing, where without the reservation the same insertions grow the image and move it. The example of this section measures it with reserve and capacity (Reserving).

Writing JSON

wd::stringify(v, indent) is JSON.stringify(v, null, indent), and writes its exact bytes: the keys in the policy’s order (Key order), strings escaped as JSON.stringify escapes them (Strings, dates and bytes), numbers as JavaScript writes them (Numbers), a date as Date.prototype.toJSON writes it, and bytes by the traits’ policy. It is a function of a handle or of a document, and a member of both, doc.stringify(2) and doc["user"].stringify():

// JSON.stringify(doc, null, 2).
std::cout << doc.stringify(2) << '\n';

The indent is JavaScript’s: 0 writes compactly, a larger one is clamped to 10 spaces, and an empty array or object is written [] or {} at any indent. A value that is undefined, a handle of nothing or a document that holds undefined, writes no text, as JSON.stringify(undefined) returns none. stringify returns a std::string, so its only failure is the standard library’s std::bad_alloc; a program that must not meet even that writes to a sink of its own.

In pieces, to a sink

wd::stringify_to(v, sink, indent, chunk) writes the same bytes as stringify, in pieces of at most chunk bytes, 4096 unless told otherwise, to a sink: any callable that takes a std::string_view and returns void or wd::result<void>. A failure the sink returns stops the writing and is returned; an exception it throws propagates. The document is unchanged either way, and a writer holds no recursion, so a document nested as deep as memory allows is written:

// The same bytes as stringify, in pieces of at most 8 bytes, to a sink that prints each.
const wd::result<void> written =
    wd::stringify_to(*made, [](std::string_view piece) { std::cout << piece << '|'; }, 0, 8);
if (written.has_error()) {
    return 1;
}
std::cout << '\n';

// A sink that returns a failure stops the writing, and stringify_to returns that failure.
std::string kept;
const wd::result<void> stopped = wd::stringify_to(
    *made,
    [&kept](std::string_view piece) -> wd::result<void> {
        if (!kept.empty()) {
            return wd::errc::capacity;
        }
        kept = piece;
        return {};
    },
    0, 8);
std::cout << kept << " then " << stopped.error().message() << '\n';
{"name":|"Ada","t|ags":["m|ath"],"n|":1500}|
{"name": then no room left

The first sink writes a bar after each piece of 8 bytes; the second keeps the first piece, then fails, which stops the writing there.

Parsing

wd::parse(text, options) is JSON.parse(text): it reads JSON text into a new owned document and returns it in a result. It accepts what RFC 8259 and JSON.parse accept, and nothing else: no comments, no trailing commas, and whitespace of space, tab, line feed and carriage return only. A key written twice keeps the last value in the first one’s place, as JavaScript does. A document’s parser never throws: its input is the network’s, and every failure is a value.

wd::parse<Traits> parses into a document of other traits, as wd::parse<wd::js_traits>(text) does in Numbers; wd::parse_into(buffer, text) parses into a buffer the program owns (Images); and wd::parse_or_throw(text), with exceptions only, throws as JSON.parse does (The sugar that throws).

Limits and revivers

wd::parse_options holds the limits of a parse, each of which is errc::capacity when it is passed, and the revivers:

Member What it is

max_depth

The deepest nesting of arrays and objects, 256 unless told otherwise.

max_bytes

The most bytes of text, every chunk counted; no limit unless told otherwise.

max_document_bytes

The largest image an owned document grows to, 64 MiB unless told otherwise.

revive_dates

Whether a string in exactly the format of Date.prototype.toJSON becomes a date (Dates).

revive_bytes

Whether text of the traits’ bytes policy becomes bytes (Bytes).

Memory per value

Every value of a document is a node of the image’s tree, and a node takes 192 bytes of the image whatever it holds: a number, true, null, an empty string, an empty array or object alike. A string’s bytes, and a key’s, take a block of their own besides: 48 bytes for a string of 8 bytes, 112 for one of 100. So "abcdefgh" takes 240 bytes, and {"k":0}, an object and a member with its key, 432. These are the image’s sizes on every target, measured, as an image is the same bytes everywhere (Images).

A short value costs far more in the image than in its text. Measured, 2 MB of text, [0,0,...] with 1,048,576 zeros, makes an image of 268 MB, 128 times the text, which shrink_to_fit() brings to 201 MB; and while an owned image grows, its old and new buffers are held at once. So the default max_document_bytes is 64 MiB, which refuses input from the network early: it is reached by 700 KB of such text, which a native release build reads for 0.4 seconds with 137 MB resident before it fails, where a limit of 1 GiB would take 11 MB of text, 9 seconds and 2.2 GB. A program that parses larger documents raises max_document_bytes to what it can afford, and sets max_bytes with it: a parse past either fails with errc::capacity. A document the program builds itself, with create, keeps a limit of 1 GiB (Images).

In chunks

wd::parser is the parser itself, wd::basic_parser<Traits, Storage> for other traits or a buffer of the program’s: write(chunk) takes the text in chunks of any size, split anywhere, inside a string, a number, an escape or a character’s bytes, down to one byte at a time, and finish() ends the text and returns the document. The document a parser builds is the one a whole parse builds, however the text is split:

wd::parser p;
// The text {"name":"Ada","tags":["math"],"n":1.5e3}, split inside a string and a number.
for (const std::string_view chunk :
     {R"({"name":"Ad)", R"(a","tags":["ma)", R"(th"],"n":1.)", R"(5e3})"}) {
    if (const wd::result<void> written = p.write(chunk); written.has_error()) {
        std::cout << "error: " << written.error().message() << '\n';
        return 1;
    }
}
wd::result<wd::document> made = p.finish();
if (made.has_error()) {
    std::cout << "error: " << made.error().message() << '\n';
    return 1;
}
std::cout << made->stringify() << '\n';
{"name":"Ada","tags":["math"],"n":1500}

The parser builds the document node by node as each chunk arrives, so the document may grow and its image move between two chunks; finish returns it. reset() makes the parser ready for another text, its selectors kept (Selectors).

Errors and offsets

Malformed text is errc::syntax, JavaScript’s SyntaxError. offset() is the count of bytes consumed, which after a syntax error is the offset where it was found, the same however the text was split, and cause() is the code of Boost.JSON’s parser behind it. A number the traits refuse is errc::range (What each refuses), and a limit passed errc::capacity.

A parser reads one text: anything but whitespace after it is errc::syntax, whose cause() is Boost.JSON’s extra_data, at the offset where the second text begins, whether it comes in the same chunk or a later one, so "1 2" and "[1] " then "[2]" are refused, and no selector sees the second text. A parser that failed stays failed: every later write and finish returns the same error, until reset. A finished parser takes no more text, which is errc::syntax, until reset. And max_bytes counts every chunk written, not each one alone:

p.reset();
report("write [1,2", p.write("[1,2"), p);
report("write ,]", p.write(",]"), p);  // the error, at the byte where it was found
std::cout << "cause: " << p.cause().message() << '\n';
report("write 3]", p.write("3]"), p);  // a failed parser stays failed
p.reset();
report("write [1]", p.write("[1]"), p);
const wd::result<wd::document> one = p.finish();
std::cout << "finish: " << (one.has_value() ? one->stringify() : one.error().message()) << '\n';
report("write [2]", p.write("[2]"), p);  // a finished parser takes no more text
wd::parse_options limits;
limits.max_depth = 2;
limits.max_bytes = 8;
wd::parser limited(limits);
report("write [[1],", limited.write("[[1],"), limited);
report("write [2]]", limited.write("[2]]"), limited);  // 9 bytes in all
std::cout << "[[[1]]]: " << wd::parse("[[[1]]]", limits).error().message() << '\n';
write [1,2: ok
write ,]: malformed JSON text at 5
cause: syntax error
write 3]: malformed JSON text at 5
write [1]: ok
finish: [1]
write [2]: malformed JSON text at 3
write [[1],: ok
write [2]]: no room left at 5
[[[1]]]: no room left

Lone surrogates

JSON.parse('"\ud800"') gives a string holding a lone surrogate, half of a UTF-16 pair. document’s parser refuses it with errc::syntax, with either encoding, utf16 included, which could store it. That is a limit of Boost.JSON’s parser, which document reads JSON with, and a deliberate choice: the parser cannot deliver a lone surrogate, but only refuse it, or, with allow_invalid_utf16, replace it with U+FFFD, which would change the text without a word. So it is documented, not worked around. A program that must hold one, with wd::utf16, stores it with set of a std::u16string_view, and stringify writes it as JSON.stringify does (Strings).

Streams of documents

A stream of documents is a stream of bytes that holds JSON texts one after another: a socket that a server writes NDJSON to, a log file of JSON lines, the output of a program that prints indented documents. wd::stream_parser, wd::basic_stream_parser<Traits> for other traits, in <webcpp/document/stream.hpp>, takes the bytes in chunks of any size, split anywhere, down to one byte at a time, and delivers each document as soon as the bytes that end it are consumed. It reads one document at a time: at a document’s end it delivers it and begins the next on the bytes that follow, in the same chunk, so a stream of any length needs the memory of the documents not yet taken and of the one being read.

Each document is parsed as wd::parse parses one text (Parsing), with the same key order, numbers and revivers, into a new owned document, and is held to the same limits on its own: max_depth, max_document_bytes, and max_bytes, which counts the bytes of one document from its first, not those of the stream (Limits and revivers). wd::stream_options holds those options, and two of its own: the framing, which says how the documents are told apart, and the policy on errors.

Framings

wd::framing The stream

concatenated

Documents one after another, with any JSON whitespace between them, or none: indented JSON, {"a":1} {"b":2}, {"a":1}{"b":2}, [1][2] and "x""y" alike. A document ends where its value closes; a top-level number ends at the next byte that cannot continue it, so 1 2 is two documents and 12 one, however it is split. The default.

lines

NDJSON, also called JSON Lines: one document per line.

rfc7464

JSON text sequences, RFC 7464: each document in a record that the byte 0x1E opens.

NDJSON

A line of NDJSON holds one document, and a document ends at its line’s \n. JSON never holds a raw \n inside a string, where it is always the escape \n, so a raw \n in the stream always ends a line. With framing::lines:

  • a document ends at its line’s \n, or at \r\n: the \r, and any other JSON whitespace before or after the document on its line, is ignored;

  • an empty line, or one of whitespace only, is ignored;

  • the last line may end without its \n: finish() closes it;

  • two documents on one line are an error for that line, and so is a document not complete at its \n. Indented JSON is therefore not NDJSON: { alone on a line is an error, and so is each line after it, until a line holds a whole document again.

A document of a line is delivered at the line’s \n, once the line is known to hold it alone.

JSON text sequences

With framing::rfc7464, a record starts at 0x1E, the ASCII record separator, and ends before the next 0x1E or at the stream’s end. Its document may span lines, indented, and is delivered as soon as its value closes; a top-level number closes at the \n that RFC 7464 puts after each text, at the next 0x1E, or at finish(). A record holds nothing but whitespace after its document; an empty record is ignored. Whitespace before the first record is ignored, and any other byte before it is an error.

Errors

A bad document is errc::syntax for malformed text, errc::range for a number the traits refuse, and errc::capacity past a limit, as in a parse (Errors and offsets). What follows it is the policy’s, wd::on_error:

wd::on_error What a bad document does

stop

It ends the stream: the documents before it are delivered, then every call returns its error, until reset(). The default, and the only policy of framing::concatenated, which has no place to go on from.

skip

It is dropped, and its error is delivered in its place, in the stream’s order; the stream goes on at the next line with framing::lines, and at the next 0x1E with framing::rfc7464.

A parser asked to skip with framing::concatenated is refused: every call of it returns boost::system::errc::invalid_argument. After an error is delivered, error_line() and error_offset() say where it was found, the line counted from 1 and the offset in bytes from the stream’s start, and error_cause() gives the code of Boost.JSON’s parser behind it, which is extra_data for a second document on a line or in a record.

finish() ends the stream: it closes a top-level number or a last line without its \n, and a document the end cuts short is errc::syntax. After it the parser takes no more bytes, which is errc::syntax, until reset(); an empty write and another finish() complete nothing.

Without I/O

The parser is sans-IO: it reads no socket and no file, so it serves any source, a socket read by hand, a file read in blocks, an actor’s messages, or JavaScript that hands chunks to a C++ function compiled with Emscripten. Its documents come out in one of two ways:

  • write(bytes) queues each document it completes, and next() takes the oldest: a result<std::optional<document>> that holds the document, nothing when none is complete, or an error, delivered as the policy says;

  • write(bytes, f) and finish(f), the push form, hand each document to f as it completes, after any that next() has not taken: f takes a result<document>, the document or, with on_error::skip, the error of one dropped, and returns void or result<void>, whose failure ends the stream with that error.

write and finish return how many documents, and dropped documents’ errors, they completed. line() is the line of the last byte consumed, offset() the bytes consumed, and documents() the documents delivered so far. The core never throws, as the rest of document: an exception that f throws propagates, with the bytes up to the document it was given consumed.

By hand

The program below reads each stream from a string, with no I/O at all. Its helpers print the error a call returned with the line it was found on, take every document the parser holds, and write the text in pieces of one, five, two and seven bytes in turn, as a socket might deliver it:

/** Prints the error a write, a finish or a take returned, with the line it was found on. */
void report_error(const wd::stream_parser& p, const boost::system::error_code& error) {
    std::cout << "error at line " << p.error_line() << ": " << error.message() << '\n';
}
/**
 Takes every document the parser holds, printing each as stringify writes it. Under
 on_error::skip a dropped document's error is printed and taking goes on; under on_error::stop the
 error comes back on every take, so taking ends at it.
*/
void take_ready(wd::stream_parser& p, wd::on_error policy) {
    for (;;) {
        wd::result<std::optional<wd::document>> taken = p.next();
        if (taken.has_error()) {
            report_error(p, taken.error());
            if (policy == wd::on_error::stop) {
                return;
            }
            continue;
        }
        const std::optional<wd::document>& held = *taken;
        if (!held.has_value()) {
            return;
        }
        std::cout << held->stringify() << '\n';
    }
}
/**
 Writes `text` to the parser in pieces of 1, 5, 2 and 7 bytes in turn, as a socket might deliver
 it, and takes the documents each piece completes. Returns false when the stream stopped.
*/
bool write_in_pieces(wd::stream_parser& p, std::string_view text, wd::on_error policy) {
    constexpr std::array<std::size_t, 4> sizes{1, 5, 2, 7};
    for (std::size_t turn = 0; !text.empty(); ++turn) {
        const std::string_view piece = text.substr(0, sizes[turn % sizes.size()]);
        text.remove_prefix(piece.size());
        if (const wd::result<std::size_t> written = p.write(piece); written.has_error()) {
            report_error(p, written.error());
            return false;
        }
        take_ready(p, policy);
    }
    return true;
}

NDJSON ends each document at its line’s newline, so a line may end in CRLF, a line may be empty, and the last line may lack its newline, which finish() closes. The documents are taken with next():

/**
 NDJSON: each document ends at its line's newline, so a line may end in CRLF, a line may be empty,
 and the last line may lack its newline, which finish closes.
*/
bool read_ndjson() {
    std::cout << "NDJSON, taken with next():\n";
    wd::stream_options options;
    options.framing = wd::framing::lines;
    wd::stream_parser p(options);
    constexpr std::string_view text =
        "{\"id\":1,\"tags\":[\"a\",\"b\"]}\n"
        "{\"id\":2,\"note\":\"first\\nsecond\"}\r\n"
        "\n"
        "[3,4.5]\n"
        "null";
    if (!write_in_pieces(p, text, options.on_error)) {
        return false;
    }
    if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
        report_error(p, finished.error());
        return false;
    }
    take_ready(p, options.on_error);
    return true;
}
NDJSON, taken with next():
{"id":1,"tags":["a","b"]}
{"id":2,"note":"first\nsecond"}
[3,4.5]
null

Concatenated JSON, with no separator or with indentation between the documents, is pushed to a callback, which sees each document as soon as its last byte is consumed:

/**
 Concatenated JSON, with no separator or with indentation between documents, pushed to a callback:
 each document is handed over as soon as its last byte is consumed, so a callback sees it before
 the rest of the piece is read.
*/
bool push_concatenated() {
    std::cout << "concatenated, pushed to a callback:\n";
    wd::stream_parser p;
    constexpr std::string_view text =
        "{\"a\":1}    {\"b\":2}\n"
        "[1][2]\n"
        "{\n"
        "  \"c\": [\n"
        "    3\n"
        "  ]\n"
        "}";
    const auto print = [](wd::result<wd::document> made) {
        std::cout << (made.has_value() ? made->stringify() : made.error().message()) << '\n';
    };
    for (std::size_t at = 0; at < text.size(); at += 3) {
        if (const wd::result<std::size_t> written = p.write(text.substr(at, 3), print);
            written.has_error()) {
            report_error(p, written.error());
            return false;
        }
    }
    if (const wd::result<std::size_t> finished = p.finish(print); finished.has_error()) {
        report_error(p, finished.error());
        return false;
    }
    std::cout << p.documents() << " documents\n";
    return true;
}
concatenated, pushed to a callback:
{"a":1}
{"b":2}
[1]
[2]
{"c":[3]}
5 documents

With on_error::skip, a bad line is reported with its line number and the stream goes on at the next line:

/**
 NDJSON with a bad line under on_error::skip: the bad line is reported with its line number, and the
 stream goes on from the next line.
*/
bool skip_bad_line() {
    std::cout << "NDJSON with a bad line, skipped:\n";
    wd::stream_options options;
    options.framing = wd::framing::lines;
    options.on_error = wd::on_error::skip;
    wd::stream_parser p(options);
    constexpr std::string_view text = "{\"ok\":1}\n{\"bad\":}\n[2]\n\"three\"";
    if (!write_in_pieces(p, text, options.on_error)) {
        return false;
    }
    if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
        report_error(p, finished.error());
        return false;
    }
    take_ready(p, options.on_error);
    return true;
}
NDJSON with a bad line, skipped:
{"ok":1}
error at line 2: malformed JSON text
[2]
"three"

And a stream of JSON text sequences, whose records open with the byte 0x1E and may span lines:

/**
 JSON text sequences, RFC 7464: each record opens with the byte 0x1E, and its document may span
 several lines.
*/
bool read_rfc7464() {
    std::cout << "rfc7464 records:\n";
    wd::stream_options options;
    options.framing = wd::framing::rfc7464;
    wd::stream_parser p(options);
    constexpr std::string_view text =
        "\x1e{\"a\":1}\n"
        "\x1e{\n  \"b\": [\n    2\n  ]\n}\n"
        "\x1e\"three\"\n";
    if (!write_in_pieces(p, text, options.on_error)) {
        return false;
    }
    if (const wd::result<std::size_t> finished = p.finish(); finished.has_error()) {
        report_error(p, finished.error());
        return false;
    }
    take_ready(p, options.on_error);
    return true;
}
rfc7464 records:
{"a":1}
{"b":[2]}
"three"

A bare number or other scalar that ends a concatenated stream is closed only by the end of the input. A document that is an object or an array ends where its value closes, but 42 has no byte that ends it: the number could go on in the next chunk. It is delivered when finish() is called, which the Asio and Cobalt readers of this section call when the stream reaches its end.

With Boost.Asio

<webcpp/document/asio.hpp> reads a stream of documents from Boost.Asio’s streams. It is the one header of document that includes Boost.Asio, which builds natively only, so <webcpp/document.hpp> leaves it out, and a program that uses it includes it and is built natively.

wd::async_read_document(stream, buffer, parser, token) reads the next document of stream, an AsyncReadStream such as a socket or a pipe. It takes the parser’s next document when the parser holds one, and otherwise reads from the stream into buffer and writes what it read into the parser, piece by piece, until a document is complete. The buffer is either of Boost.Asio’s two models: a DynamicBuffer_v2, such as what asio::dynamic_buffer makes of a string, is taken by value and only what it names must outlive the operation; a DynamicBuffer_v1, such as Beast’s multi_buffer, is taken by reference and must outlive it. Bytes the buffer already holds are read first, and a buffer made of several pieces is never joined into one. It is a composed operation that takes any completion token of Boost.Asio’s, a callback, asio::use_awaitable, asio::deferred or boost::cobalt::use_op among them, and completes with (boost::system::error_code, document):

Error The completion

none

The next document.

the error of a document dropped

With on_error::skip; the next call goes on with the stream.

the error that stopped the stream

With on_error::stop, every time, until the parser’s reset().

asio::error::eof

The stream ended, and the parser’s finish() delivered every document it held; every later call completes with it again.

another

The error of a read.

On an error the document is empty. A completion is never called inside async_read_document: one that needs no read is dispatched to the handler’s immediate executor, or posted to the stream’s. One operation at a time reads a stream, its buffer and its parser, which outlive it.

The program below runs a server and a client on the loopback address, in one thread. The server writes NDJSON, then concatenated JSON, in pieces of one, three, seven, two and thirteen bytes, so the client’s parser meets documents cut in the middle. The connection is a pair of sockets:

/**
 A TCP connection on the loopback address: the server's end, written to, and the client's end,
 read from.
*/
struct link {
    link(asio::io_context& io, tcp::acceptor& acceptor) : server(io), client(io) {
        client.connect(acceptor.local_endpoint());
        acceptor.accept(server);
    }

    tcp::socket server;
    tcp::socket client;
};

The server’s end is written in pieces, and after each piece the context is drained until the client has consumed it, so that the output does not depend on timing; the write side is then shut down, which the client reads as the end of the stream:

/**
 Writes `text` to the server's end in pieces of 1, 3, 7, 2 and 13 bytes in turn. After each piece
 the context is drained until the client's parser has consumed it, so the pieces arrive one at a
 time; then the write side is shut down, which the client reads as eof.
*/
bool send_in_pieces(asio::io_context& io, tcp::socket& server, std::string_view text,
                    const wd::stream_parser& parser) {
    constexpr std::array<std::size_t, 5> sizes{1, 3, 7, 2, 13};
    std::uint64_t sent = 0;
    for (std::size_t turn = 0; !text.empty(); ++turn) {
        const std::string_view piece = text.substr(0, sizes[turn % sizes.size()]);
        text.remove_prefix(piece.size());
        asio::write(server, asio::buffer(piece.data(), piece.size()));
        sent += piece.size();
        if (!drain_until(io, [&parser, sent] { return parser.offset() == sent; })) {
            return false;
        }
    }
    server.shutdown(tcp::socket::shutdown_send);
    return true;
}

The client reads the NDJSON in a coroutine, awaiting async_read_document in a loop until the completion is eof:

/**
 Reads the client's documents until eof, awaiting each read in turn. The parser's finish delivers
 its last document before the eof.
*/
asio::awaitable<void> read_in_coroutine(tcp::socket& client, wd::stream_parser& parser) {
    std::string storage;
    const auto buffer = asio::dynamic_buffer(storage);
    for (;;) {
        auto [ec, made] = co_await wd::async_read_document(client, buffer, parser,
                                                           asio::as_tuple(asio::use_awaitable));
        if (ec == asio::error::eof) {
            std::cout << "eof\n";
            co_return;
        }
        if (ec) {
            std::cout << "error: " << ec.message() << '\n';
            co_return;
        }
        std::cout << made.stringify() << '\n';
    }
}

/** Sends NDJSON over a link and reads it in a coroutine, one document printed per line. */
bool ndjson_in_coroutine(asio::io_context& io, tcp::acceptor& acceptor) {
    std::cout << "NDJSON, read in a coroutine:\n";
    link c(io, acceptor);
    wd::stream_options options;
    options.framing = wd::framing::lines;
    wd::stream_parser parser(options);
    constexpr std::string_view text =
        "{\"id\":1,\"tags\":[\"a\",\"b\"]}\n"
        "{\"id\":2,\"ok\":false}\r\n"
        "\n"
        "[3,null]\n"
        "\"last\"";
    bool done = false;
    asio::co_spawn(io, read_in_coroutine(c.client, parser),
                   [&done](const std::exception_ptr&) { done = true; });
    return send_in_pieces(io, c.server, text, parser) && drain_until(io, [&done] { return done; });
}
NDJSON, read in a coroutine:
{"id":1,"tags":["a","b"]}
{"id":2,"ok":false}
[3,null]
"last"
eof

and the concatenated JSON with a callback, which starts the next read by posting it, so that no read begins inside its own callback. The last document of this stream is the bare number 42, which arrives only when eof makes the reader finish the parser:

/**
 Reads documents with a callback, each read started after the one before it completes. The next
 read is posted rather than called, so no read is started from inside its own callback.
*/
class callback_reader {
public:
    callback_reader(tcp::socket& client, wd::stream_parser& parser, bool& done)
        : client_(client), parser_(parser), done_(done), buffer_(asio::dynamic_buffer(storage_)) {}

    /** Starts the read of the next document. */
    void start() {
        wd::async_read_document(client_, buffer_, parser_,
                                [this](boost::system::error_code ec, const wd::document& made) {
                                    if (ec == asio::error::eof) {
                                        std::cout << "eof\n";
                                        done_ = true;
                                        return;
                                    }
                                    if (ec) {
                                        std::cout << "error: " << ec.message() << '\n';
                                        done_ = true;
                                        return;
                                    }
                                    std::cout << made.stringify() << '\n';
                                    asio::post(client_.get_executor(), [this] { start(); });
                                });
    }

private:
    tcp::socket& client_;
    wd::stream_parser& parser_;
    bool& done_;
    std::string storage_;
    asio::dynamic_string_buffer<char, std::char_traits<char>, std::allocator<char>> buffer_;
};

/** Sends concatenated JSON over a link and reads it with a callback. */
bool concatenated_with_callback(asio::io_context& io, tcp::acceptor& acceptor) {
    std::cout << "concatenated JSON, read with a callback:\n";
    link c(io, acceptor);
    wd::stream_parser parser;
    constexpr std::string_view text =
        "{\"a\":1}    {\"b\":[true,null]}\n"
        "[1][2]\n"
        "{\n  \"c\": \"x\"\n}\n"
        "42";
    bool done = false;
    callback_reader reader(c.client, parser, done);
    reader.start();
    return send_in_pieces(io, c.server, text, parser) && drain_until(io, [&done] { return done; });
}
concatenated JSON, read with a callback:
{"a":1}
{"b":[true,null]}
[1]
[2]
{"c":"x"}
42
eof

After an HTTP header, with Boost.Beast

A response’s header is read with Boost.Beast, and the body that follows it is read as a stream of documents from the same buffer. Beast reads the socket in blocks, so the bytes of the body that arrived with the header are in the buffer when async_read_header returns. They stay there, past the header, and async_read_document takes them before it reads the socket again: nothing is copied out of the buffer, and nothing is lost.

The program below runs a server and a client on the loopback address, in one thread. The client sends a GET, and the server answers with a header that Beast writes and an NDJSON body in pieces. The header and the body’s first piece leave in one write, so the client’s first read holds both, which is the case this section is about:

/**
 Writes `bytes` to the socket; false when the write failed, after printing the error.
*/
asio::awaitable<bool> send_bytes(tcp::socket& socket, std::string_view bytes) {
    auto [ec, sent] = co_await asio::async_write(socket, asio::buffer(bytes.data(), bytes.size()),
                                                 asio::as_tuple(asio::use_awaitable));
    if (ec) {
        std::cout << "server: " << ec.message() << '\n';
        co_return false;
    }
    co_return true;
}

/**
 Answers the client's GET: the response header, written by Beast, and the NDJSON body in pieces of
 2, 5, 3 and 11 bytes; the write side is then shut down, which ends the body for the client, since
 the response has no Content-Length.
*/
asio::awaitable<void> serve(tcp::socket& socket) {
    constexpr std::string_view body =
        "{\"id\":1,\"name\":\"ada\"}\n"
        "{\"id\":2,\"tags\":[\"a\",\"b\"]}\n"
        "[3,null]\n"
        "\"last\"";

    beast::multi_buffer buffer;
    http::request<http::empty_body> request;
    auto [read_ec, read_bytes] =
        co_await http::async_read(socket, buffer, request, asio::as_tuple(asio::use_awaitable));
    if (read_ec) {
        std::cout << "server: " << read_ec.message() << '\n';
        co_return;
    }

    // Without a Content-Length the body ends with the connection, so the connection is not kept.
    http::response<http::empty_body> response{http::status::ok, request.version()};
    response.set(http::field::content_type, "application/x-ndjson");
    response.keep_alive(false);
    std::ostringstream header;
    header << response;

    // The header and the body's first piece leave in one write, so the client's first read can take
    // both: the bytes past the header are then in the client's buffer when Beast returns.
    constexpr std::array<std::size_t, 4> sizes{2, 5, 3, 11};
    std::string_view rest = body;
    std::string first = header.str();
    const std::string_view head_piece = rest.substr(0, sizes[0]);
    rest.remove_prefix(head_piece.size());
    first.append(head_piece);
    if (!co_await send_bytes(socket, first)) {
        co_return;
    }
    for (std::size_t turn = 1; !rest.empty(); ++turn) {
        const std::string_view piece = rest.substr(0, sizes[turn % sizes.size()]);
        rest.remove_prefix(piece.size());
        if (!co_await send_bytes(socket, piece)) {
            co_return;
        }
    }
    socket.shutdown(tcp::socket::shutdown_send);
}

The client reads the header into a beast::multi_buffer with a response_parser<empty_body>, and prints the status and the content type. Beast does not read the body: the same buffer goes on to async_read_document, with the document parser of the stream:

beast::multi_buffer buffer;
http::response_parser<http::empty_body> header;
auto [header_ec, header_bytes] = co_await http::async_read_header(
    socket, buffer, header, asio::as_tuple(asio::use_awaitable));
if (header_ec) {
    std::cout << "client: " << header_ec.message() << '\n';
    co_return;
}
const http::response<http::empty_body>& response = header.get();
std::cout << "status " << response.result_int() << ' ' << response.reason() << '\n';
std::cout << "content type " << response[http::field::content_type] << '\n';
// The buffer may already hold the first bytes of the body: whatever Beast read past the header
// stays in it. The reader writes those bytes into the parser before it reads the socket again,
// so the first document is parsed from them, and nothing is copied out of the buffer.
wd::stream_options options;
options.framing = wd::framing::lines;
wd::stream_parser documents(options);
for (;;) {
    auto [ec, made] = co_await wd::async_read_document(socket, buffer, documents,
                                                       asio::as_tuple(asio::use_awaitable));
    if (ec == asio::error::eof) {
        std::cout << "eof\n";
        co_return;
    }
    if (ec) {
        std::cout << "error: " << ec.message() << '\n';
        co_return;
    }
    std::cout << made.stringify() << '\n';
}
status 200 OK
content type application/x-ndjson
{"id":1,"name":"ada"}
{"id":2,"tags":["a","b"]}
[3,null]
"last"
eof

With Boost.Cobalt

async_read_document takes boost::cobalt::use_op as it takes any token, so a Cobalt task reads a stream of documents in the style of Cobalt, and cobalt::as_result turns the completion into a result whose error is the stream’s, eof included. One task writes the lines in pieces, another reads each document and prints it with its number, until the end of the stream, which the last line ends without a newline:

// Sends the text in pieces of 1, 3, 7, 2 and 13 bytes in turn, then shuts the sending side of the
// connection, which is the end of the stream for the reader.
cobalt::task<void> send(tcp::socket& socket, std::string_view text) {
    constexpr std::array<std::size_t, 5> sizes{1, 3, 7, 2, 13};
    for (std::size_t turn = 0; !text.empty(); ++turn) {
        const std::size_t size = std::min(sizes[turn % sizes.size()], text.size());
        co_await asio::async_write(socket, asio::buffer(text.data(), size), cobalt::use_op);
        text.remove_prefix(size);
    }
    socket.shutdown(tcp::socket::shutdown_send);
}
// Reads the documents one at a time and prints each with its number; the end of the stream is
// eof, which is not an error, and a failure of the stream is printed and ends the reader.
cobalt::task<void> read(tcp::socket& socket) {
    wd::stream_options options;
    options.framing = wd::framing::lines;
    wd::stream_parser parser(options);
    std::string storage;
    const auto buffer = asio::dynamic_buffer(storage);
    std::size_t count = 0;
    for (;;) {
        auto next = co_await cobalt::as_result(
            wd::async_read_document(socket, buffer, parser, cobalt::use_op));
        if (next.has_error()) {
            if (next.error() == asio::error::eof) {
                break;
            }
            std::cout << "error: " << next.error().message() << '\n';
            co_return;
        }
        ++count;
        std::cout << count << ' ' << next->stringify() << '\n';
    }
    std::cout << "eof after " << count << " documents\n";
}
// The connection is made before the coroutines start: the listening socket holds it until accept.
cobalt::main co_main(int /*argc*/, char** /*argv*/) {
    const auto executor = cobalt::this_thread::get_executor();
    tcp::acceptor acceptor(executor, tcp::endpoint(asio::ip::address_v4::loopback(), 0));
    tcp::socket writer(executor);
    tcp::socket reader(executor);
    reader.connect(acceptor.local_endpoint());
    acceptor.accept(writer);
    co_await cobalt::join(send(writer, ndjson), read(reader));
    co_return 0;
}
1 {"name":"Ada","tags":["math"],"n":1500}
2 [1,2.5,null,true,"x"]
3 "plain text"
4 {"z":1,"a":2}
5 {"last":true}
eof after 5 documents

In an actor system

A parser can live inside an actor, because it is sans-IO: the actor gives it bytes and takes its documents. The program below runs two actors on xactor’s FIFO driver, which makes its output the same on every run. The messages are the bytes of the stream, its end, a document the parser completed, and a line it dropped:

/** The bytes of the stream, as the host sends them. */
struct chunk {
    std::string text;
};

/** The end of the stream, which the parser closes. */
struct end_of_stream {};

/** A document the parser completed, moved to the consumer: its image stays where it is. */
struct parsed {
    wd::document value;
};

/** A line the parser dropped, and the line it was on. */
struct rejected {
    std::string reason;
    std::uint64_t line = 0;
};

using message = std::variant<chunk, end_of_stream, parsed, rejected>;

The parser actor writes each chunk it receives to a stream_parser, whose push form hands each document to a callback that sends it to the consumer. The document is moved in the message, and its image stays where it is. A line that is not JSON is dropped, and the actor reports it and goes on:

/** Parses the stream, and sends each document, or each line it drops, to the consumer. */
class parser final : public xactor::actor_logic<message> {
public:
    explicit parser(xactor::actor_ref consumer) : consumer_(consumer), stream_(lines_skipping()) {}

    /** The callback the parser hands each document to, and each dropped line's error. */
    auto hand(xactor::turn<message>& turn) {
        return
            [this, &turn](wd::result<wd::document>&& made) { return relay(turn, std::move(made)); };
    }

    xactor::result<void> handle(xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* bytes = std::get_if<chunk>(&cause.payload)) {
            const xactor::result<std::size_t> written = stream_.write(bytes->text, hand(turn));
            return as_void(written);
        }
        if (std::holds_alternative<end_of_stream>(cause.payload)) {
            const xactor::result<std::size_t> finished = stream_.finish(hand(turn));
            return as_void(finished);
        }
        return {};
    }

private:
    /** Sends a completed document, or the reason a line was dropped, to the consumer. */
    xactor::result<void> relay(xactor::turn<message>& turn, wd::result<wd::document>&& made) {
        if (made.has_value()) {
            return turn.send(consumer_, parsed{.value = std::move(*made)});
        }
        return turn.send(consumer_,
                         rejected{.reason = made.error().message(), .line = stream_.error_line()});
    }

    /** Keeps the failure of a call of the parser; the documents it completed went on already. */
    static xactor::result<void> as_void(const xactor::result<std::size_t>& counted) {
        if (!counted.has_value()) {
            return {boost::system::in_place_error, counted.error()};
        }
        return {};
    }

    xactor::actor_ref consumer_;
    wd::stream_parser stream_;
};

The consumer prints what it receives:

/** Prints each document with stringify, and each line the parser dropped. */
class consumer final : public xactor::actor_logic<message> {
public:
    xactor::result<void> handle([[maybe_unused]] xactor::turn<message>& turn,
                                const xactor::envelope<message>& cause) override {
        if (const auto* got = std::get_if<parsed>(&cause.payload)) {
            std::cout << got->value.stringify() << '\n';
        }
        if (const auto* dropped = std::get_if<rejected>(&cause.payload)) {
            std::cout << "line " << dropped->line << " dropped: " << dropped->reason << '\n';
        }
        return {};
    }
};

and the host delivers the chunks one at a time, cut inside a string, a number and a line, and runs the actors until they are idle after each:

xactor::scheduler<message> system(xactor::budgets{.fuel = 100});
const xactor::result<xactor::actor_ref> printing = xactor::create_actor<consumer>(system);
if (!printing.has_value()) {
    return 1;
}
const xactor::result<xactor::actor_ref> reading =
    xactor::create_actor<parser>(system, *printing);
if (!reading.has_value()) {
    return 1;
}

// Two documents and a line that is not JSON, in chunks cut inside a string, a number and a
// line; a third document with an exponent, and a last one with no newline after it.
const std::array<std::string_view, 7> pieces{
    R"({"name":"Ad)",
    R"(a","born":181)",
    "5}\n{\"name\":\"Gr",
    R"(ace","langs":["COB)",
    "OL\"]}\nnot j",
    "son\n{\"name\":\"Alan\",\"n\":1.5",
    "e3}\n{\"name\":\"Edsger\"}",
};

xactor::fifo_driver<message> driver(system);
std::uint64_t delivery = 0;
for (const std::string_view piece : pieces) {
    const xactor::correlation_id execution{.value = ++delivery};
    if (!system.deliver(*reading, chunk{.text = std::string(piece)}, execution).has_value()) {
        return 1;
    }
    if (!driver.run_until_idle(nullptr).has_value()) {
        return 1;
    }
}
const xactor::correlation_id execution{.value = ++delivery};
if (!system.deliver(*reading, end_of_stream{}, execution).has_value()) {
    return 1;
}
if (!driver.run_until_idle(nullptr).has_value()) {
    return 1;
}
{"name":"Ada","born":1815}
{"name":"Grace","langs":["COBOL"]}
line 3 dropped: malformed JSON text
{"name":"Alan","n":1500}
{"name":"Edsger"}

Fed from JavaScript

A sender written in JavaScript can feed the parser compiled from C++ chunk by chunk, and a ReadableStream such as a response body can be turned into an async iterable of documents. The module that does this is taught in Streams; the program below shows the same parser in a program of your own, compiled with Emscripten, which JavaScript drives through Embind.

The class wraps a stream_parser and holds two JavaScript functions. JavaScript writes the bytes of the stream to it, and its callback runs once for each document that a chunk completes, with the text stringify writes, or for each line it drops, with the line and the message. Embind turns a Uint8Array into the std::string of write, so a chunk may end inside a character:

/**
 A parser of a stream of documents that JavaScript writes to and that calls JavaScript back: the
 callback `on_document` is given each document as the text `stringify` writes, and `on_error` the
 line and the message of an error.

 The framing `lines` is NDJSON, whose bad line is dropped and reported while the stream goes
 on; any other name is a concatenated stream, whose first error ends it.
*/
class json_stream {
public:
    json_stream(const std::string& framing, emscripten::val on_document, emscripten::val on_error)
        : on_document_(std::move(on_document)),
          on_error_(std::move(on_error)),
          parser_(options_of(framing)) {}

    /** Takes a chunk of the stream, and calls back for each document it completes. */
    void write(const std::string& chunk) {
        const wd::result<std::size_t> written =
            parser_.write(chunk, [this](auto made) { deliver(std::move(made)); });
        if (written.has_error()) {
            report(written.error());
        }
    }

    /** Ends the stream, which closes a last line with no newline and a number at the end. */
    void finish() {
        const wd::result<std::size_t> finished =
            parser_.finish([this](auto made) { deliver(std::move(made)); });
        if (finished.has_error()) {
            report(finished.error());
        }
    }

private:
    static wd::stream_options options_of(std::string_view framing) {
        wd::stream_options options;
        if (framing == "lines") {
            options.framing = wd::framing::lines;
            options.on_error = wd::on_error::skip;
        }
        return options;
    }

    /** What the parser gives for each document it completes, or for each line it drops. */
    void deliver(wd::result<wd::document> made) {
        if (made.has_error()) {
            report(made.error());
            return;
        }
        on_document_(made->stringify());
    }

    void report(const boost::system::error_code& error) {
        on_error_(static_cast<unsigned>(parser_.error_line()), error.message());
    }

    emscripten::val on_document_;
    emscripten::val on_error_;
    wd::stream_parser parser_;
};
EMSCRIPTEN_BINDINGS(document_stream_js) {
    emscripten::class_<json_stream>("JsonStream")
        .constructor<const std::string&, emscripten::val, emscripten::val>()
        .function("write", &json_stream::write)
        .function("finish", &json_stream::finish);
}

Node loads the module and sends two streams in chunks, one of NDJSON, whose bad line is dropped and reported while the stream goes on, and one concatenated, whose last number only finish closes. The fifth chunk of the NDJSON stream ends with the byte 0xC3, the first of the two bytes of the é of Zoé:

// Sends a text to a C++ stream in chunks of the given size and prints what each chunk completes.
// The text is encoded first, so that a chunk may end inside a character, as a socket's may.
function send(framing, text, size) {
    let chunk = 0
    const stream = new module.JsonStream(framing,
        document => lines.push(`  chunk ${chunk}: ${document}`),
        (line, message) => lines.push(`  chunk ${chunk}: line ${line} dropped, ${message}`))
    const bytes = encoder.encode(text)
    lines.push(`${framing}, in chunks of ${size} bytes:`)
    for (let at = 0; at < bytes.length; at += size) {
        chunk += 1
        stream.write(bytes.subarray(at, at + size))
    }
    chunk += 1
    stream.finish()
    stream.delete()
}
send('lines', '{"id":1,"tags":["a","b"]}\n{"id":2,"name":"Zoé"}\n{"bad":}\n[3,4.5]\n"last"', 9)
send('concatenated', '{"a":1}    {"b":[2,3]}[4]"five" 6', 4)

Each line it prints says the chunk after which a document arrived. The last line of the NDJSON stream, and the last number of the other, arrive when finish ends the stream, as nothing before ends them:

lines, in chunks of 9 bytes:
  chunk 3: {"id":1,"tags":["a","b"]}
  chunk 6: {"id":2,"name":"Zoé"}
  chunk 7: line 3 dropped, malformed JSON text
  chunk 8: [3,4.5]
  chunk 9: "last"
concatenated, in chunks of 4 bytes:
  chunk 2: {"a":1}
  chunk 6: {"b":[2,3]}
  chunk 7: [4]
  chunk 8: "five"
  chunk 10: 6

The program is a driven test: webcpp.drive builds it with Emscripten and runs Node over it, in the own lane js (Using it from JavaScript), where Node compares what it prints with these lines.

Selectors

A stream of a million records should not need the memory of a million records. A parser’s selectors name places in the text, and deliver each value there as soon as it is complete, while the rest of the text is still to come: on delivers a value and drops it from the document being built, and observe delivers it and keeps it.

The language

A selector is a subset of JSONPath: $, the root, then any sequence of these steps.

Step What it matches

.name

The member name. A name starts with a letter, _, $ or a byte past ASCII, and goes on with those and digits.

['name']

The member name, any key, with \' and \\ as its only escapes.

[n]

The element n, written in decimal without a leading zero.

[*]

Every element of an array.

.*

Every member of an object.

So $.items[*].id matches the id of each element of items. A malformed selector is errc::type, returned by on or observe.

Delivery

A callback takes a wd::const_value, the value, which it reads but cannot change, and a wd::path_view, where the value is in the text, valid during the call; to_string() writes it as a selector of that place, $['items'][0], which on parses back. A key’s ' and \ are escaped and its other bytes written as they are, control characters included, so the text is not RFC 9535’s normalized path. A callback returns void or wd::result<void>. The program sums the quantities of items as they stream, and keeps meta:

/** A callback that prints what it receives and where. */
constexpr auto print = [](const wd::const_value& v, wd::path_view where) {
    std::cout << "  " << where.to_string() << ": " << v.stringify() << '\n';
};
constexpr std::string_view text =
    R"({"meta":{"page":1},"items":[{"id":"a","qty":2},{"id":"b","qty":3}],"done":true})";
wd::parser p;
std::int64_t total = 0;
const wd::result<void> taken =
    p.on("$.items[*]", [&total](const wd::const_value& item, wd::path_view where) {
        print(item, where);
        total += item["qty"].value_or(std::int64_t{0});
    });
const wd::result<void> kept = p.observe("$.meta", print);
if (taken.has_error() || kept.has_error() || p.write(text).has_error()) {
    return 1;
}
std::cout << "total " << total << '\n';
finished(p);
  $['meta']: {"page":1}
  $['items'][0]: {"id":"a","qty":2}
  $['items'][1]: {"id":"b","qty":3}
total 5
finish: {"meta":{"page":1},"items":[],"done":true}

A scalar is delivered when its token ends, and an array or an object when it closes. A value inside another that also matches is delivered first, since it completes first; if on took it, the outer value no longer holds it. A path’s indices are those of the text, which on does not renumber:

wd::parser rows;
if (rows.on("$.rows[*][1]", print).has_error() ||
    rows.observe("$.rows[*]", print).has_error() ||
    rows.write(R"({"rows":[[1,2,3],[4,5,6]]})").has_error()) {
    return 1;
}
finished(rows);
  $['rows'][0][1]: 2
  $['rows'][0]: [1,3]
  $['rows'][1][1]: 5
  $['rows'][1]: [4,6]
finish: {"rows":[[1,3],[4,6]]}

on("$") takes the root, and finish then returns a document that holds undefined. A number at the top of the text completes only when the text ends, since another digit could follow, so it is delivered by finish:

wd::parser whole;
if (whole.on("$", print).has_error() || whole.write("[1,2]").has_error()) {
    return 1;
}
finished(whole);  // the root was taken
whole.reset();    // the selectors stay
if (whole.write("12").has_error()) {
    return 1;
}
std::cout << "12 written\n";  // a number at the top ends only with the text
finished(whole);
  $: [1,2]
finish: undefined
12 written
  $: 12
finish: undefined

A failing callback

A failure a callback returns fails the parser: write returns it, and so does every later call, until reset. In a build with exceptions, an exception a callback throws fails the parser the same way: write or finish rethrows it, and every later call throws it again, until reset.

wd::parser strict;
const wd::result<void> subscribed =
    strict.on("$[*]", [](const wd::const_value& v, wd::path_view) -> wd::result<void> {
        if (!v.is_number()) {
            return wd::errc::type;
        }
        return {};
    });
if (subscribed.has_error()) {
    return 1;
}
std::cout << "write: " << strict.write(R"([1,"two",3])").error().message() << '\n';
finished(strict);  // a failed parser stays failed until reset
write: a value of another kind
finish: a value of another kind

In bounded memory

A value on takes is erased from the document once delivered, its blocks zeroed and freed, so the next one takes the same room: a stream of any length needs the memory of one element at a time. A callback that must keep a value copies it, into a document of its own, with set or clone.

wd::parser stream;
std::uint64_t count = 0;
if (stream.on("$[*]", [&count](const wd::const_value&, wd::path_view) { ++count; })
        .has_error() ||
    stream.write("[").has_error()) {
    return 1;
}
for (std::uint64_t i = 0; i < 100'000; ++i) {
    const std::string element =
        std::string(i == 0 ? "" : ",") + R"({"id":)" + std::to_string(i) + "}";
    if (stream.write(element).has_error()) {
        return 1;
    }
}
if (stream.write("]").has_error()) {
    return 1;
}
const wd::result<wd::document> streamed = stream.finish();
if (streamed.has_error()) {
    return 1;
}
std::cout << count << " delivered, " << streamed->stringify()
          << " left, image under 64 KiB: " << std::boolalpha
          << (streamed->image().size() < 65'536) << '\n';
100000 delivered, [] left, image under 64 KiB: true

Traits

A document’s traits choose what it stores and how: the C++ types of its scalars, the encoding of its strings, how it writes bytes, and the order of an object’s keys. wd::basic_document<Traits, Storage> takes them first; wd::document is wd::basic_document<wd::default_traits, wd::owned>, and its handles wd::value and wd::const_value are those of the default traits too.

Member wd::default_traits wd::js_traits

boolean

bool

bool

integer

std::int64_t

std::int64_t

number

boost::decimal::decimal128_t: exact, 34 significant digits

double: JavaScript’s own numbers

string

wd::utf8

wd::utf16, as JavaScript holds strings

date

wd::date_ms

wd::date_ms

bytes

wd::base64

wd::javascript_bytes, as JSON.stringify writes a Uint8Array

object

wd::javascript_order

wd::javascript_order

array

wd::dense

wd::dense

js_traits derive from the default traits, and a program’s traits do the same, changing what they need:

/** The default traits, but every key in the order it was created, whatever it is. */
struct insertion_traits : wd::default_traits {
    using object = wd::insertion_order;
};

/** The default traits, but keys sorted by their code units, and bytes written as base64url. */
struct canonical_traits : wd::default_traits {
    using object = wd::sorted;
    using bytes = wd::base64url;
};

Key order parses a text with both, and Bytes writes bytes with the second. The concept wd::document_traits says what traits must give, and a document of traits that do not satisfy it does not compile. This version fixes boolean, integer and date to bool, std::int64_t and wd::date_ms; number is the traits’ choice, a type that satisfies wd::stored_scalar, trivially copyable, of 1, 2, 4, 8 or 16 bytes, with a wd::scalar_codec that stores it the same way on every target and a wd::number_codec that reads it from JSON text and writes it as JavaScript writes numbers. string, bytes and object are one of the encodings and policies Strings, dates and bytes and Key order list, and array is wd::dense, the one array policy.

Traits are part of the image

A document’s image records its traits: its header holds a tag that hashes the size and the codec of each scalar, the encoding, the bytes policy and the object policy, with the sizes of the tree’s nodes. An image written with other traits is refused with errc::incompatible_document, never read wrongly (What is refused). Copies between documents need the same traits, and a document’s handles carry them in their type, so a handle of one traits cannot be given to a document of another.

Images

A document lives in one block of memory, its image: a header of 64 bytes, then a heap that holds the tree, its nodes, its indices and its strings. Every pointer inside is an offset from where it is stored, and every size a 64-bit integer, so the image works at any 16-aligned address and has the same bytes on a 64-bit native target and on wasm32. A document can therefore live in memory it does not own, such as a browser’s, be copied as bytes, be written to a file and read back, and travel as binary between a program’s processes. This section teaches the image itself: its bytes, its header, what opening refuses, where it lives and how its size is managed. Images on the wire sends images over streams and Sharing shares them between threads and processes.

The bytes

doc.image() is the image, a std::span<const std::byte> valid until the document’s next change. wd::from_image(bytes) copies an image into a new owned document, from any address; doc.clone() does the same with a document’s own:

wd::result<wd::document> made = wd::parse(R"({"user":"Ada","langs":["Analytical","Note G"]})");
if (made.has_error()) {
    return false;
}
const std::span<const std::byte> image = made->image();
std::cout << image.size() << " bytes, sha-256 " << digest_of(image) << "...\n";

// An owned copy, at another address, which grows as any owned document does.
wd::result<wd::document> copy = wd::from_image(image);
if (copy.has_error() || !(*copy)["user"].set("Lovelace")) {
    return false;
}
std::cout << copy->stringify() << '\n' << made->stringify() << '\n';
4096 bytes, sha-256 ed85084394e7bf7f...
{"user":"Lovelace","langs":["Analytical","Note G"]}
{"user":"Ada","langs":["Analytical","Note G"]}

The same operations give the same image, byte for byte, at any address and on every target: the digest above is the one every target of webcpp’s continuous integration prints, natively with Clang and GCC, on emscripten, and on wasm32-wasip2 and wasm32-wasip3. Free memory is zero: a buffer is zeroed when it is made or grown, a block is zeroed before it is freed, and nodes are built in place. So an erased value, a secret among them, leaves none of its bytes in the image, and the image is a function of the operations alone.

After an operation that failed for want of room, the document’s content is as it was, but bytes of the heap’s free space may differ, where the heap keeps its own bookkeeping: compare documents with ==, not their raw images, after a failure.

The header

The header says what the bytes after it are, in the byte order of the machine that wrote it:

Offset Size Field

0

8

the magic, WCPPDOC and a zero byte

8

4

the format version, 1

12

4

the byte-order mark, 0x01020304 as the writer’s machine writes it

16

4

reserved, zero; an image whose reserved bytes are not zero is refused

20

4

the layout tag: a hash of the sizes and alignments of the tree’s node, its container and the heap, and of the traits (Traits are part of the image)

24

8

the size of the image

32

8

the offset of the tree’s container, the heap’s first block, the same in every image

40

8

the id the next node takes; ids are never reused

48

8

the seal: zero for an image that is not sealed, else the high bit and the CRC-32C of the image, with these eight bytes taken as zero (Sealed images)

56

8

zero

The format version is the library’s: webcpp::document::format_version, which the library raises when the layout of an image changes, and then provides a migration from the previous version. The Boost release an image was written with is not in the header, and a Boost upgrade does not change the version, since the structures the image holds, Boost.Interprocess’s rbtree_best_fit blocks and Boost.MultiIndex’s ordered index nodes, rarely change. The library watches them: its format test compares the compact image of a fixed document, under each traits, with golden images committed with the tests, and fails when a Boost release changes one byte of them. The bytes at offset 16 are kept for a future version.

What is refused

wd::open and wd::from_image refuse bytes shorter than the header or than the size it states, and another magic, with errc::not_a_document; and an image of another format version, byte order, layout tag or nonzero reserved bytes, with errc::incompatible_document. A mismatch is refused, never read or converted. Then the heap’s own check of its structure runs, and a container offset other than the heap’s first block, or a pointer to the root that leaves the image, is errc::not_a_document too, refused before anything is read through it. A sealed image whose bytes do not match its seal is errc::damaged_document, and is checked first of all (Sealed images).

std::cout << "16 zero bytes: " << wd::from_image(std::array<std::byte, 16>{}).error().message()
          << '\n';
std::cout << "other traits: " << wd::from_image<wd::js_traits>(image).error().message() << '\n';
16 zero bytes: not a document's image
other traits: an image of another format, layout, byte order or traits

Each refusal has its own cause, and a program tells them apart by the errc of the error. This program makes one of each and prints the code and the message: an image of other traits, an image of another format version, text that is longer than a header and has no magic, zeroed memory, a buffer that is not 16-aligned (since open reads in place), an image cut short, a header that states a size larger than the bytes it is given, and a header that names a container outside the heap. The last line is the same image cut short after it was sealed (Sealed images):

// An image written with default_traits is no image of js_traits: the layout tag differs.
if (!refused("js_traits", wd::from_image<wd::js_traits>(image))) {
    return false;
}
// The format version is the header's field at offset 8; another version is not read.
std::span<std::byte> bytes = copy_into(storage, image);
set_field(bytes, offsetof(wd::image_header, version), std::uint32_t{2});
if (!refused("format version 2", wd::open(bytes))) {
    return false;
}
// Bytes that are not an image, and are longer than a header, have no magic.
constexpr std::string_view text =
    "this text is not an image, and it is longer than the 64 bytes of a header";
if (!refused("garbage", wd::from_image(std::as_bytes(std::span(text))))) {
    return false;
}
// Memory that was never written reads as zeros, which are no magic either.
alignas(16) std::array<std::byte, 256> zeros{};
if (!refused("zeroed memory", wd::open(zeros))) {
    return false;
}
// open reads the image in place, so its buffer must be 16-aligned: this one starts 8 bytes in.
bytes = copy_into(std::span<std::byte>(storage).subspan(8), image);
if (!refused("misaligned buffer", wd::open(bytes))) {
    return false;
}
// Cut short, an image that is not sealed is no image at all.
bytes = copy_into(storage, image);
if (!refused("truncated, unsealed", wd::open(bytes.first(bytes.size() - 16)))) {
    return false;
}
// A header that states a size larger than the bytes it is given is refused before it is read.
bytes = copy_into(storage, image);
set_field(bytes, offsetof(wd::image_header, size), std::uint64_t{image.size() + 4096});
if (!refused("size", wd::open(bytes.first(image.size())))) {
    return false;
}
// A header that names a container outside the heap is refused before anything is read there.
bytes = copy_into(storage, image);
set_field(bytes, offsetof(wd::image_header, container), std::uint64_t{image.size()});
return refused("container offset", wd::open(bytes));
// Cut short, a sealed image is a torn write: damaged, not merely no image.
std::span<std::byte> bytes = copy_into(storage, made->image());
if (!refused("truncated, sealed", wd::open(bytes.first(bytes.size() - 16)))) {
    return false;
}
js_traits: incompatible_document, an image of another format, layout, byte order or traits
format version 2: incompatible_document, an image of another format, layout, byte order or traits
garbage: not_a_document, not a document's image
zeroed memory: not_a_document, not a document's image
misaligned buffer: not_a_document, not a document's image
truncated, unsealed: not_a_document, not a document's image
size: not_a_document, not a document's image
container offset: not_a_document, not a document's image
truncated, sealed: damaged_document, a sealed image whose bytes do not match its seal

An image is trusted input. Those checks cannot prove sound every offset inside the tree, so an image travels only between a program’s own processes and peers it trusts; untrusted data enters as JSON text, which the parser checks entirely. A seal (Sealed images) catches bytes that changed by accident and is not authentication: never open an image from a source you do not trust.

Owned and external storage

A document’s second template parameter is its storage:

Storage The buffer When it is full

wd::owned, the default

the document’s own, 16-aligned

it grows: a buffer twice the size, or what the operation needs if more, the image copied and the heap grown, up to the max_bytes of create or from_image, 1 GiB unless told otherwise; past it, errc::capacity

wd::external

a std::span<std::byte> its user owns and keeps alive, such as a browser’s memory, 16-aligned

the operation returns errc::capacity and changes nothing

wd::format(buffer) makes a new, empty document in a buffer, holding null; wd::open(buffer) opens the image a buffer holds, in place; wd::parse_into(buffer, text) parses into one. Every operation finds its room before it allocates, so a full buffer is an error value, never a half-made change. A buffer that is not 16-aligned is errc::capacity to format, which finds no room for an image there, and errc::not_a_document to open.

// A buffer the program owns, such as a browser's memory, 16-aligned.
alignas(16) std::array<std::byte, 8192> buffer{};
using external_document = wd::basic_document<wd::default_traits, wd::external>;
wd::result<external_document> formatted = wd::format(buffer);
if (formatted.has_error() || !formatted->root().set(wd::array{})) {
    return false;
}
wd::basic_value<wd::default_traits, wd::external> list = formatted->root();
std::uint64_t pushed = 0;
wd::result<std::uint64_t> length = list.push("an element of the list");
for (; length.has_value(); length = list.push("an element of the list")) {
    ++pushed;
}
const std::string before = formatted->stringify();
std::cout << pushed << " pushed, then: " << length.error().message() << '\n';
// A failed operation changes nothing of the content.
std::cout << "unchanged: " << std::boolalpha
          << (list.splice(0, 1, wd::array{1, 2, 3}).error() == wd::errc::capacity) << ' '
          << (formatted->stringify() == before) << '\n';

// The buffer holds the image, which another document opens in place.
wd::result<external_document> opened = wd::open(buffer);
if (opened.has_error()) {
    return false;
}
std::cout << "opened in place: " << opened->root().size() << " elements\n";
31 pushed, then: no room left
unchanged: true true
opened in place: 31 elements

Reserving

A document that is to be built large grows by doubling: each time the image is full, a buffer twice the size is made, the image is copied into it and the old one is freed. reserve(bytes) is std::vector::reserve for an image: it makes the image hold at least bytes in one step, so that a batch of insertions of a known size moves nothing, and capacity() says how large the image is. A reservation moves the image once, which invalidates every std::string_view read from a value but no handle or iterator, and clears a seal; a reservation within the capacity changes nothing. A request past the document’s max_bytes, or past an external image’s size, is errc::capacity and changes nothing.

This program inserts ten thousand members into two objects, one without a reservation and one with a reservation of 256 bytes a member, and asks whether the image grew:

// The batch's size is known, so the image is reserved once, before the insertions: they then
// move nothing. Without the reservation, the same insertions grow the image.
constexpr std::int64_t batch = 10'000;
constexpr std::uint64_t bytes_per_member = 256;

wd::result<wd::document> plain_made = wd::parse("{}");
if (plain_made.has_error()) {
    return fail(plain_made);
}
wd::document& plain = *plain_made;
wd::result<wd::object_ref> as_plain = plain.root().as_object();
if (as_plain.has_error()) {
    return fail(as_plain);
}
const std::uint64_t plain_before = plain.capacity();
for (std::int64_t i = 0; i < batch; ++i) {
    const auto added = as_plain->insert("k" + std::to_string(i), i);
    if (added.has_error()) {
        return fail(added);
    }
}
std::cout << "without reserve, " << batch
          << " insertions grew the image: " << (plain.capacity() != plain_before) << '\n';

wd::result<wd::document> reserved_made = wd::parse("{}");
if (reserved_made.has_error()) {
    return fail(reserved_made);
}
wd::document& reserved = *reserved_made;
wd::result<wd::object_ref> as_reserved = reserved.root().as_object();
if (as_reserved.has_error()) {
    return fail(as_reserved);
}
const std::uint64_t reserved_before = reserved.capacity();
const auto reservation = reserved.reserve(static_cast<std::uint64_t>(batch) * bytes_per_member);
if (reservation.has_error()) {
    return fail(reservation);
}
const std::uint64_t reserved_capacity = reserved.capacity();
std::cout << "reserve grew the image: " << (reserved_capacity != reserved_before) << '\n';
for (std::int64_t i = 0; i < batch; ++i) {
    const auto added = as_reserved->insert("k" + std::to_string(i), i);
    if (added.has_error()) {
        return fail(added);
    }
}
std::cout << "after the reserve, " << batch
          << " insertions grew it: " << (reserved.capacity() != reserved_capacity) << '\n';
without reserve, 10000 insertions grew the image: true
reserve grew the image: true
after the reserve, 10000 insertions grew it: false

Shrinking

A document that is parsed starts at 4096 bytes, and a document that is built grows from there as it needs. doc.shrink_to_fit() gives back the free space at the end of an owned image, and only that: blocks are never moved, so a value that sits at the end of the heap keeps the image as large as its end. For a small document it is the whole story, a few values shrinking to little more than a kilobyte. For a document that grew and then let its values go, it may leave an image nearly as large as it was, and a deep copy into a new document packs the content, as compact does in place (Compacting):

wd::result<wd::document> made = wd::parse("{}");
if (made.has_error()) {
    return false;
}
for (std::uint64_t i = 0; i < 1000; ++i) {
    if (!(*made)[std::to_string(i)].set(std::string(100, 'x'))) {
        return false;
    }
}
const std::uint64_t grown = made->image().size();
if (!made->root().set(wd::object{{"kept", true}}) || !made->shrink_to_fit()) {
    return false;
}
std::cout << "grown to " << grown << " bytes, shrunk to " << made->image().size() << ": "
          << made->stringify() << '\n';

// Blocks never move: a deep copy into a new document packs the content.
wd::result<wd::document> packed = wd::document::create();
if (packed.has_error() || !packed->root().set(*made)) {
    return false;
}
std::cout << "packed: " << packed->image().size() << " bytes\n";
grown to 524288 bytes, shrunk to 352864: {"kept":true}
packed: 4096 bytes

Compacting

doc.compact() rebuilds an owned image as the compact image of its content: the nodes in document order, their ids numbered from the first, each block right after the last with no hole between, and the image exactly as large as they are. Where shrink_to_fit can only trim the free tail, compact takes out every hole, so a document that lost most of its values is smaller compacted than shrunk. Measured, the same on a 64-bit target and on wasm32: a small document of 60 members after 54 of them were erased is 15,312 bytes after shrink_to_fit and 2,352 after compact; a large one, with 90% of its members erased, is 2,080,304 bytes and 209,424.

Compacting also makes the image a function of the content alone. Two documents of equal content have byte-identical images after it, whatever edits made them, whether they were parsed, or built by set, erase and splice, or had values replaced; and a compact image compacts to itself. Equal documents can then be compared, hashed or stored by their bytes, and a seal over a compact image is the same everywhere.

compact_size() is the exact size of the compact image, which it reads from the nodes without allocating anything, so a program sizes a block with it before it builds the image. compact_into(buffer) writes the compact image into a 16-aligned buffer of the program’s own and opens it there, as an external document, zeroing the rest of the buffer and leaving the document as it was: it is the way to compact a document that cannot reallocate, such as one leased from a pool or held in a shared block, or to send an image without waste. It is errc::capacity for a buffer that is smaller than compact_size() or not aligned, and writes nothing then.

Compacting removes holes, not the 192 bytes of each node (Memory per value), so it does not change how an image compares in size with the JSON text of the same document (The sizes on the wire).

compact() builds the new image before it replaces the old one, so a failure changes nothing. As a vector’s reallocation does, it invalidates every handle, every std::string_view read from a value and every iterator, and, the bytes having changed, it clears a seal: sealing again is the caller’s. The image’s size afterwards is compact_size(), which is also its capacity(), so the next change that needs room grows it.

A moved document takes the image of the one it is moved from, which then holds nothing: its handles, and those of the document moved into, are invalid.

Images on the wire

A document is its image, and its image can be sent as it is: the sender writes the bytes, and the receiver reads them into a block of memory and opens the document there. Nothing is parsed, no tree is built, and nothing is allocated for the values or for the message; the receiver’s work is the copy of the bytes off the stream and, for a sealed image, one pass to check the seal. JSON text needs a parse on arrival and allocates for each value it holds.

That is the use document’s image is built for, between programs that trust each other: a page and its Worker, the threads of a process, the processes of a machine, two machines on a fast local link whose receiver’s CPU is the bottleneck (Binary transmission). It is not the use for a narrow network: an image is many times the size of its JSON text, and larger than the compressed text (The sizes on the wire), so there the text is the smaller payload. The benchmarks measure what the receiver saves on each side of the trade (Reading the results).

This section sends images over a stream with Boost.Asio and receives them into a pool of blocks; sealing them, what a reader refuses and how big a block must be follow. Sharing shares images between threads and processes without any stream.

An image frames itself

An image states its size in its header (The header), so a stream of images needs no separator and no length prefix: the images follow one another, and a reader that has the first 64 bytes of one knows where it ends. wd::async_write_image(stream, doc, token) seals the document and writes its image as one buffer. wd::async_read_image(stream, pool, token) reads the header of the next image into a block of a pool, checks it before it reads another byte, reads the rest of the image into the same block and opens the document there. It reads exactly the bytes the image needs, so the stream is left at the end of the image it completed.

Both are composed operations of Boost.Asio, in <webcpp/document/asio.hpp>, which take any completion token, and are built natively only, as the rest of that header is (With Boost.Asio). async_write_image completes with an error_code; async_read_image completes with an error_code and a wd::leased_document<>, which holds the document and the block it lives in.

Pools of blocks

wd::image_pool::create(block_bytes, blocks) allocates a pool once: blocks blocks of block_bytes bytes each, 16-aligned and zeroed, in one allocation. A block is leased to each image a reader receives, and goes back when the leased_document that holds it goes, its bytes zeroed. The pool therefore allocates nothing per message; a receiver that can hold two images at once makes a pool of two blocks:

/** A pool of two blocks: the most the client leases at once. */
wd::image_pool pool_of_two() {
    return wd::image_pool::create(block_bytes, 2).value();
}

When every block is leased, async_read_image completes with errc::capacity and reads nothing: this is the backpressure of a stream of images, and the stream is read again when a block is given back. A pool outlives its leases and its readers, and is not thread-safe: a block is leased and given back, a lease destroyed included, on one thread at a time. For blocks that threads and processes share, see Shared documents.

A leased_document is moved, never copied, and reads and writes as any document does within its image; a change that needs more room than its block has returns errc::capacity. The sans-IO wd::image_reader under async_read_image is the same reader without Asio: write(bytes) takes the bytes of a stream and opens each image they complete, and prepare() and commit() let a socket read straight into the block, with no copy at all.

Sizing a block

A block holds the largest image the pool will receive, a multiple of 16 and at least 64: an image larger than a block is refused with errc::capacity. What a document costs on the wire is its image, whatever it holds.

A document that is parsed starts at 4096 bytes, as the parser gives it room, so a document of a few values sent as it is travels as 4096 bytes. shrink_to_fit() gives back the free space at the end of the image, and only that: for a small document it brings the image down to little more than a kilobyte (Shrinking). compact() rebuilds the image with no hole in it, and compact_size() says how large that image is before it is built (Compacting), so a program sizes its blocks, and what it sends, to what a document holds. This function makes the documents of the next example, parsing each and shrinking it before it is sent:

/**
 A document parsed from JSON text and shrunk to its size, so that its image holds what the document
 needs and no more.
*/
wd::document document_of(std::string_view text) {
    wd::document made = wd::parse_or_throw(text);
    // A shrink that fails leaves the document as valid as it was, only larger.
    static_cast<void>(made.shrink_to_fit());
    return made;
}

/** The documents the server sends, in order. */
std::vector<wd::document> documents() {
    std::vector<wd::document> sent;
    sent.push_back(document_of(ada));
    sent.push_back(document_of(grace));
    sent.push_back(document_of(alan));
    return sent;
}

Sending and receiving

The example sends three documents over a connection on the loopback address, in one program, and receives them twice, once in a coroutine and once with a callback. The receiver prints each document with stringify while its image is held in its block, whether the image is sealed, and the blocks the pool has free then:

/**
 Prints a document as the client holds it: its text, whether its image is sealed, and the blocks the
 pool has free while the image's block is leased.
*/
void print_held(const wd::leased_document<>& image, const wd::image_pool& pool) {
    std::cout << image.get().stringify() << ": " << (image.sealed() ? "sealed" : "not sealed")
              << ", blocks available: " << pool.available() << '\n';
}

The coroutine sends each image, awaiting each write, and shuts the write side down at the end, which the reader sees as eof. The receiver awaits async_read_image in a loop. Each image is printed while its block is leased, and the block goes back when the iteration ends:

/** Sends each document's image, awaiting each write, then shuts the write side down. */
asio::awaitable<void> send_in_coroutine(tcp::socket& server, std::vector<wd::document>& sent) {
    for (wd::document& document : sent) {
        auto [ec] =
            co_await wd::async_write_image(server, document, asio::as_tuple(asio::use_awaitable));
        if (ec) {
            std::cout << "error: " << ec.message() << '\n';
            co_return;
        }
    }
    server.shutdown(tcp::socket::shutdown_send);
}

/**
 Receives images until eof, awaiting each read in turn. Each image is printed while its block is
 leased, and the block goes back when the loop's iteration ends.
*/
asio::awaitable<void> receive_in_coroutine(tcp::socket& client, wd::image_pool& pool, bool& done) {
    for (;;) {
        auto [ec, image] =
            co_await wd::async_read_image(client, pool, asio::as_tuple(asio::use_awaitable));
        if (ec == asio::error::eof) {
            std::cout << "eof, blocks available: " << pool.available() << '\n';
            done = true;
            co_return;
        }
        if (ec) {
            std::cout << "error: " << ec.message() << '\n';
            done = true;
            co_return;
        }
        print_held(image, pool);
    }
}

/** Sends the documents over a link and receives them in a coroutine. */
bool images_in_coroutine(asio::io_context& io, tcp::acceptor& acceptor,
                         std::vector<wd::document>& sent) {
    std::cout << "images, received in a coroutine:\n";
    link c(io, acceptor);
    wd::image_pool pool = pool_of_two();
    bool done = false;
    asio::co_spawn(io, send_in_coroutine(c.server, sent), asio::detached);
    asio::co_spawn(io, receive_in_coroutine(c.client, pool, done), asio::detached);
    return drain_until(io, [&done] { return done; });
}
images, received in a coroutine:
{"name":"Ada","born":1815,"langs":["Analytical"]}: sealed, blocks available: 1
{"name":"Grace","langs":["COBOL"]}: sealed, blocks available: 1
{"name":"Alan","n":1500}: sealed, blocks available: 1
eof, blocks available: 2

With a pool of two blocks and one document held, one block is free, and at eof both are. With callbacks, the next write and the next read are posted rather than called, so that none starts inside the callback of the one before it:

/**
 Sends each document's image with async_write_image, the next one started after the last one
 completed, then shuts the write side down. The next write is posted, not called, so that no write
 starts inside the callback of the one before it.
*/
class callback_sender {
public:
    callback_sender(tcp::socket& server, std::vector<wd::document>& sent)
        : server_(server), sent_(sent) {}

    /** Starts the first write. */
    void start() { send_next(); }

private:
    void send_next() {
        if (next_ == sent_.size()) {
            server_.shutdown(tcp::socket::shutdown_send);
            return;
        }
        wd::async_write_image(server_, sent_[next_], [this](boost::system::error_code ec) {
            if (ec) {
                std::cout << "error: " << ec.message() << '\n';
                return;
            }
            ++next_;
            asio::post(server_.get_executor(), [this] { send_next(); });
        });
    }

    tcp::socket& server_;
    std::vector<wd::document>& sent_;
    std::size_t next_ = 0;
};

/**
 Receives images with async_read_image, the next read started after the last one completed, until
 eof. The image is printed inside its callback, where its block is leased; the block goes back when
 the callback returns.
*/
class callback_receiver {
public:
    callback_receiver(tcp::socket& client, wd::image_pool& pool, bool& done)
        : client_(client), pool_(pool), done_(done) {}

    /** Starts the first read. */
    void start() { read_next(); }

private:
    void read_next() {
        wd::async_read_image(
            client_, pool_, [this](boost::system::error_code ec, wd::leased_document<> image) {
                if (ec == asio::error::eof) {
                    std::cout << "eof, blocks available: " << pool_.available() << '\n';
                    done_ = true;
                    return;
                }
                if (ec) {
                    std::cout << "error: " << ec.message() << '\n';
                    done_ = true;
                    return;
                }
                print_held(image, pool_);
                asio::post(client_.get_executor(), [this] { read_next(); });
            });
    }

    tcp::socket& client_;
    wd::image_pool& pool_;
    bool& done_;
};

/** Sends the documents over a link and receives them with a callback. */
bool images_with_callback(asio::io_context& io, tcp::acceptor& acceptor,
                          std::vector<wd::document>& sent) {
    std::cout << "images, received with a callback:\n";
    link c(io, acceptor);
    wd::image_pool pool = pool_of_two();
    bool done = false;
    callback_sender sender(c.server, sent);
    callback_receiver receiver(c.client, pool, done);
    sender.start();
    receiver.start();
    return drain_until(io, [&done] { return done; });
}
images, received with a callback:
{"name":"Ada","born":1815,"langs":["Analytical"]}: sealed, blocks available: 1
{"name":"Grace","langs":["COBOL"]}: sealed, blocks available: 1
{"name":"Alan","n":1500}: sealed, blocks available: 1
eof, blocks available: 2

Several images in one write

wd::async_write_images(stream, documents, token) writes the images of a range of documents in one gather write. Every document is sealed first, and when one fails to seal the operation completes with its error and writes nothing. The images are not copied: Boost.Asio sends them with writev and carries on over partial writes. The operation completes with the bytes written, which are the sum of the images’ sizes; on an error, they are the bytes that reached the stream, and the whole documents among them are the leading ones whose images fit in that count. The range is taken by lvalue reference, and its documents stay unchanged until the write completes. The example sends its three documents this way:

/**
 Sends the three images in one gather write with async_write_images, then receives them in a
 coroutine, as the first exchange does.

 Tip: the write reads the images in place and completes with the bytes it wrote, so the documents
 stay unchanged until it completes.
*/
bool images_in_one_write(asio::io_context& io, tcp::acceptor& acceptor,
                         std::vector<wd::document>& sent) {
    std::cout << "images, sent in one write:\n";
    link c(io, acceptor);
    wd::image_pool pool = pool_of_two();
    bool done = false;
    wd::async_write_images(c.server, sent, [&c](boost::system::error_code ec, std::size_t bytes) {
        if (ec) {
            std::cout << "error: " << ec.message() << '\n';
        } else {
            std::cout << "written: " << bytes << " bytes\n";
        }
        c.server.shutdown(tcp::socket::shutdown_send);
    });
    asio::co_spawn(io, receive_in_coroutine(c.client, pool, done), asio::detached);
    return drain_until(io, [&done] { return done; });
}
images, sent in one write:
written: 4176 bytes
{"name":"Ada","born":1815,"langs":["Analytical"]}: sealed, blocks available: 1
{"name":"Grace","langs":["COBOL"]}: sealed, blocks available: 1
{"name":"Alan","n":1500}: sealed, blocks available: 1
eof, blocks available: 2

Sealed images

doc.seal() writes a seal into the image’s header: the CRC-32C (Castagnoli) of the image’s bytes, with the seal’s own eight bytes taken as zero, and a marker in the high bit. async_write_image seals the document it sends. Opening a sealed image, with wd::open, wd::from_image or the image reader, checks the seal first of all, before anything else its header states is trusted, and refuses an image whose bytes do not match with errc::damaged_document. A flipped bit on a disk or a wire, a torn write, a stray write over a buffer and an image cut short are all damage, and none of the image is read:

// A sealed image with one byte changed is refused as damaged, by open and by from_image.
const auto found =
    std::ranges::search(bytes, std::as_bytes(std::span(std::string_view("Ada"))));
if (found.empty()) {
    return false;
}
found.front() = std::byte{'a'};
if (!refused("one byte changed", wd::open(bytes))) {
    return false;
}
one byte changed: damaged_document, a sealed image whose bytes do not match its seal

The seal holds until the document’s next change, which clears it: an image is then simply not sealed, never wrongly sealed, and sealed() says which. It costs a pass over the image, about 600 MB/s, and checking it when the image is opened costs the same. A program that trusts its memory, such as one that opens an image it has just written, skips the check with wd::verify::none. The changed byte of the example then reads as it is:

// verify::none skips the seal: the caller trusts the source, and the changed byte reads as is.
wd::result<wd::document> trusted =
    wd::from_image(bytes, wd::default_max_bytes, wd::verify::none);
if (trusted.has_error()) {
    return false;
}
std::cout << "verify::none, accepted: " << trusted->stringify() << '\n';
verify::none, accepted: {"user":"ada","langs":["Analytical","Note G"]}

A sealed image left in reused memory, which a later use wrote over in part, is refused too, since the seal shows that the bytes changed since the image was written. An image that is not damaged opens in place, and is still sealed:

// An old sealed image left in memory, which a later use wrote over in part, is refused as
// damaged: the seal shows that the bytes changed since the image was written.
bytes = copy_into(storage, made->image());
std::ranges::fill(bytes.subspan(2048, 64), std::byte{0xAA});
if (!refused("reused memory", wd::open(bytes))) {
    return false;
}
// An intact sealed image opens in place, and is still sealed.
bytes = copy_into(storage, made->image());
wd::result<wd::basic_document<wd::default_traits, wd::external>> opened = wd::open(bytes);
if (opened.has_error()) {
    return false;
}
std::cout << "opened, sealed " << std::boolalpha << opened->sealed() << ": "
          << opened->stringify() << '\n';
reused memory: damaged_document, a sealed image whose bytes do not match its seal
opened, sealed true: {"user":"Ada","langs":["Analytical","Note G"]}

A seal is a checksum against accidental damage, and is not authentication. Anyone can compute a valid seal for any bytes, so a sealed image from a source that is not trusted is no safer than an unsealed one. Never open an image from an untrusted source: the checks of What is refused cannot prove sound every offset inside a tree, and an image travels only between programs that trust each other. Untrusted data enters as JSON text, which the parser checks entirely.

What a reader refuses

The completion of async_read_image says what happened, and on every error the lease it carries is empty:

Error When

none

An image was read and opened; the lease holds it.

asio::error::eof

The stream ended between two images.

errc::capacity

Every block of the pool is leased, and the stream is not read; or an image is larger than a block. The end of the stream is reported this way too while the pool is full, since the reader needs a block to learn that the stream ended.

errc::not_a_document

Bytes that are not an image, or an image whose heap is not sound.

errc::incompatible_document

An image of another format version, byte order, reserved bytes or traits.

errc::damaged_document

A sealed image whose bytes do not match its seal.

another

The error of a read.

The refusal of an image ends the reader, as an error ends a stream of documents (Errors), because a stream of images that has lost its place cannot find the next header. The refusals are the ones of What is refused, which that section shows one by one.

Sharing

JavaScript shares an object by sharing a reference to it, and its collector decides when the object goes. A C++ program that shares a document between threads or processes needs the same two things, a reference that several holders have and a moment when the last one lets go, and document gives them without a collector and without copying the document. Its image is relocatable and holds offsets, not addresses, so it is the same bytes in every thread and in every process that maps it, at whatever address.

Threads

A document that moves from one thread to another moves its handle and never the bytes of its image. A producer parses documents and pushes each into a queue; the consumer receives them and keeps them. The queue is the program’s, of any kind; this one wakes the consumer on a push and tells it when nothing more comes:

// A queue for one producer and one consumer: a push wakes the consumer, and close tells it that
// nothing more comes.
template <class T>
class queue {
public:
    void push(T value) {
        {
            const std::scoped_lock lock(mutex_);
            items_.push_back(std::move(value));
        }
        ready_.notify_one();
    }

    void close() {
        {
            const std::scoped_lock lock(mutex_);
            closed_ = true;
        }
        ready_.notify_all();
    }

    // The oldest value, waiting while the queue is open; nothing once it is closed and empty.
    std::optional<T> pop() {
        std::unique_lock lock(mutex_);
        ready_.wait(lock, [this] { return closed_ || !items_.empty(); });
        if (items_.empty()) {
            return std::nullopt;
        }
        T value = std::move(items_.front());
        items_.pop_front();
        return value;
    }

private:
    std::mutex mutex_;
    std::condition_variable ready_;
    std::deque<T> items_;
    bool closed_ = false;
};

What the producer sends is the document and the address of its image before the push, so that the consumer can show the image did not move:

// What the producer sends: a document, and the address of its image before the push. Moving a
// document moves its handle and never the bytes of its image, so the consumer finds them there.
struct parcel {
    wd::document doc;
    const std::byte* image = nullptr;
};

// What the consumer keeps: the documents in arrival order, how many of them still have their image
// where the producer left it, and how many texts failed to parse.
struct exchanged {
    std::vector<wd::document> documents;
    std::size_t same_image = 0;
    std::size_t failed = 0;
};

// The producer parses each text and pushes the document; the consumer keeps each one it receives.
// The two threads write different members of the result, and main reads it after both are joined.
exchanged through_a_queue(std::array<std::string_view, 3> texts) {
    queue<parcel> arrivals;
    exchanged result;
    std::thread consumer([&arrivals, &result] {
        while (std::optional<parcel> next = arrivals.pop()) {
            if (next->doc.image().data() == next->image) {
                ++result.same_image;
            }
            result.documents.push_back(std::move(next->doc));
        }
    });
    std::thread producer([&arrivals, &result, texts] {
        for (const std::string_view text : texts) {
            wd::result<wd::document> parsed = wd::parse(text);
            if (parsed.has_error()) {
                ++result.failed;
                continue;
            }
            const std::byte* image = parsed->image().data();
            arrivals.push(parcel{.doc = std::move(*parsed), .image = image});
        }
        arrivals.close();
    });
    producer.join();
    consumer.join();
    return result;
}
queue: 3 received, 3 with the image the producer sent, 0 failed to parse
{"id":1,"name":"Ada"}
[1,2,3]
{"id":3,"tags":["queue","thread"]}

Every document arrived with the image the producer sent: no byte was copied and nothing was parsed on the consumer’s side.

Shared documents

Several threads that read one document share it with wd::shared_document<>, which is std::shared_ptr for a document: copies share one image, a count says how many hold it, and the last holder to let go gives the image back. The images live in the blocks of a wd::shared_pool, over a region of memory the program gives, the process heap or a segment of shared memory. A pool is formatted once, wd::shared_pool::format(region, block_bytes), with a 16-aligned region; its region_bytes(block_bytes, blocks) sizes the region for a number of blocks, blocks(), block_bytes() and available() describe it, and wd::shared_pool::attach(region) is how another view of the same region, in this thread or another process, joins it.

A block is a header of 64 bytes, which holds the count of references to its image, and the room of one image. The pool works through offsets from the region’s start and never addresses, its counts are lock-free atomics and its free blocks a lock-free stack: no lock is held at any moment, so no thread or process waits on another, and one that dies leaves no lock behind. A block that is free is all zero, and a block given back is zeroed.

shared_document<>::from_image(pool, bytes) copies an image into a free block, create(pool) makes a new empty document in one, and use_count() says how many hold the document, in every process. Copying a shared_document adds a reference, destroying one drops it, and a reader takes a const reference and never writes. A reader holds its copy until it returns:

// The name and birth year of a shared document, read through a const reference: a reader never
// writes.
std::string describe(const wd::shared_document<>& doc) {
    return std::string(doc["name"].value_or(std::string_view("nobody"))) + " was born in " +
           std::to_string(doc["born"].value_or(std::int64_t{0}));
}

// A reader holds its copy until it returns, then drops it in its own thread.
void reader_task(const wd::shared_document<>& holder, std::string& out) {
    out = describe(holder);
}

The image of a parsed document keeps the room the parser gave it, 4096 bytes at first, and a block of 2048 would refuse it, so the example shrinks the image to its content before it copies it into the pool (Shrinking). Four readers then share the document, and the count is the number of holders:

// The image is copied into a block of the pool; the document it makes holds the one reference.
wd::result<wd::shared_document<>> stored =
    wd::shared_document<>::from_image(pool, parsed->image());
if (stored.has_error()) {
    std::cout << "error: " << stored.error().message() << '\n';
    return 1;
}
wd::shared_document<> shared = std::move(*stored);
std::cout << "stored: use_count " << shared.use_count() << ", " << pool.available() << " of "
          << pool.blocks() << " blocks free\n";

// Each reader gets its copy here, before any thread starts, so the count printed is exact.
constexpr std::size_t reader_count = 4;
std::vector<wd::shared_document<>> copies(reader_count, shared);
std::cout << "with " << reader_count << " readers: use_count " << shared.use_count() << '\n';

std::array<std::string, reader_count> read{};
std::vector<std::thread> readers;
readers.reserve(reader_count);
for (std::size_t i = 0; i < reader_count; ++i) {
    readers.emplace_back(reader_task, std::move(copies[i]), std::ref(read[i]));
}
for (std::thread& reader : readers) {
    reader.join();
}
std::cout << "after the readers: use_count " << shared.use_count() << ", " << pool.available()
          << " of " << pool.blocks() << " blocks free\n";
for (std::size_t i = 0; i < reader_count; ++i) {
    std::cout << "reader " << i << ": " << read[i] << '\n';
}
pool: 7 blocks of 2048 bytes
stored: use_count 1, 6 of 7 blocks free
with 4 readers: use_count 5
after the readers: use_count 1, 6 of 7 blocks free
reader 0: Ada was born in 1815
reader 1: Ada was born in 1815
reader 2: Ada was born in 1815
reader 3: Ada was born in 1815

Copy on write

A holder that wants to write calls make_mutable(). When it is the only holder, the document is its own and nothing happens. When others hold the image, make_mutable copies it into a free block of its own, the copy including the seal, and the holder changes the copy while the others keep the document as it was. It is errc::capacity when every block is leased.

The library does not synchronise writers. A block that has several writers, or a writer and readers of that same block, must wait for each other by the program’s own means, such as a mutex; the writer below changes only the copy that make_mutable made, which no other holder reads:

// make_mutable copies the image into a block of the writer's own when other holders exist, so the
// writer changes its copy and the others keep the document as it was. The library does not
// synchronise writers: a block with several writers, or a writer and readers of that same block,
// must wait for each other by the program's own means, such as a mutex. This writer changes only
// its copy, which no other holder reads.
void writer_task(wd::shared_document<> holder, std::string& out) {
    if (holder.make_mutable().has_error() || holder["name"].set("Lovelace").has_error()) {
        out = "error";
        return;
    }
    out = describe(holder);
}
// The writer takes its reference before its thread starts; its change leaves the shared
// document, which main still reads, as it was.
wd::shared_document<> writer_copy = shared;
std::string written;
std::thread writer(writer_task, std::move(writer_copy), std::ref(written));
writer.join();
std::cout << "writer: " << written << '\n';
std::cout << "after the writer: " << describe(shared) << ", use_count " << shared.use_count()
          << ", " << pool.available() << " of " << pool.blocks() << " blocks free\n";

// The last copy gives its block back to the pool, zeroed.
shared = wd::shared_document<>();
std::cout << "last copy dropped: " << pool.available() << " of " << pool.blocks()
          << " blocks free\n";
writer: Lovelace was born in 1815
after the writer: Ada was born in 1815, use_count 1, 6 of 7 blocks free
last copy dropped: 7 of 7 blocks free

The change was made to the writer’s copy, so the document that main holds still reads Ada, and the last holder to let go returned its block to the pool.

Processes

A pool in a segment of shared memory is shared by processes, each of which maps the segment at an address of its own, and a document is passed between them by the offset of its image. The program below is its own consumer: the producer formats a pool of eight blocks of 4096 bytes in Boost.Interprocess’s shared memory, parses or builds four documents in blocks of it, seals each, and sends the offsets through a message queue, and then runs itself again as the consumer, which attaches to the same segment. The constants say how many blocks and how large, and the end of the stream is an offset no image has:

// The room of each block's image, and the blocks of the pool: 8 blocks of 4096 bytes, the size of
// a parsed document's image.
constexpr std::size_t block_bytes = 4096;
constexpr std::size_t blocks = 8;

// The queue holds every offset at once, the end of the stream included.
constexpr std::size_t queue_messages = 8;

// The end of the stream: no document's image is at offset 0.
constexpr std::uint64_t end_of_stream = 0;

The producer makes each document in a block of the pool. release() gives up the producer’s reference without dropping it, and returns the offset of the image, which is the document now as far as the consumer is concerned:

// The document a JSON text makes: parsed, its image copied into a block of the pool, then sealed.
wd::result<wd::shared_document<>> sealed_from_text(wd::shared_pool pool, std::string_view text) {
    wd::result<wd::document> parsed = wd::parse(text);
    if (parsed.has_error()) {
        return parsed.error();
    }
    wd::result<wd::shared_document<>> made =
        wd::shared_document<>::from_image(pool, parsed->image());
    if (made.has_error()) {
        return made.error();
    }
    if (made->seal().has_error()) {
        return wd::errc::type;
    }
    return made;
}

// Releases a document's reference and sends its offset, the document now the consumer's.
void hand_over(bip::message_queue& queue, wd::shared_document<>& doc) {
    const std::uint64_t offset = doc.release();
    queue.send(&offset, sizeof offset, 0);
}

// The producer: it makes the documents, hands each over and runs the consumer, which prints them.
int produce(const char* program) {
    const names run = names_for(std::to_string(bip::ipcdetail::get_current_process_id()));
    remove_names(run);
    const remover cleanup(run);

    const wd::result<std::size_t> size = wd::shared_pool::region_bytes(block_bytes, blocks);
    if (size.has_error()) {
        std::cout << "error: " << size.error().message() << '\n';
        return 1;
    }
    bip::shared_memory_object memory(bip::create_only, run.segment.c_str(), bip::read_write);
    memory.truncate(static_cast<bip::offset_t>(*size));
    const bip::mapped_region mapped(memory, bip::read_write);
    const std::span<std::byte> bytes(static_cast<std::byte*>(mapped.get_address()),
                                     mapped.get_size());
    const wd::result<wd::shared_pool> formatted = wd::shared_pool::format(bytes, block_bytes);
    if (formatted.has_error()) {
        std::cout << "error: " << formatted.error().message() << '\n';
        return 1;
    }
    const wd::shared_pool pool = *formatted;
    bip::message_queue queue(bip::create_only, run.queue.c_str(), queue_messages,
                             sizeof(std::uint64_t));

    // Three documents parsed or built in place, and a fourth whose sealed image is then damaged.
    wd::result<wd::shared_document<>> ada =
        sealed_from_text(pool, R"({"name":"Ada","born":1815,"langs":["Analytical"]})");
    wd::result<wd::shared_document<>> grace =
        sealed_from_text(pool, R"({"name":"Grace","langs":["COBOL"]})");
    wd::result<wd::shared_document<>> alan = wd::shared_document<>::create(pool);
    wd::result<wd::shared_document<>> edsger = sealed_from_text(pool, R"({"name":"Edsger"})");
    if (ada.has_error() || grace.has_error() || alan.has_error() || edsger.has_error()) {
        std::cout << "error: a document could not be made\n";
        return 1;
    }
    if (alan->root().set(wd::object{{"name", "Alan"}, {"n", 1500}}).has_error() ||
        alan->seal().has_error()) {
        std::cout << "error: a document could not be made\n";
        return 1;
    }

    // One byte of the sealed image changes after the seal; any change breaks a seal.
    constexpr std::size_t damaged_byte = 100;
    pool.region()[edsger->offset() + damaged_byte] ^= std::byte{0xff};

    hand_over(queue, *ada);
    hand_over(queue, *grace);
    hand_over(queue, *alan);
    hand_over(queue, *edsger);
    queue.send(&end_of_stream, sizeof end_of_stream, 0);

    std::cout << "available before the consumer: " << pool.available() << '\n' << std::flush;

    // The consumer is this program run again, as Boost.Interprocess's own tests run theirs; the
    // parent waits for it, so its output comes before the parent's last line.
    const std::string command =
        "\"" + std::string(program) + "\" consumer " + run.segment + " " + run.queue;
    // NOLINTNEXTLINE(bugprone-command-processor,cert-env33-c)
    if (std::system(command.c_str()) != 0) {
        std::cout << "error: the consumer failed\n";
        return 1;
    }
    std::cout << "available after the consumer: " << pool.available() << '\n';
    return 0;
}

The consumer attaches to the segment, and for each offset it receives, adopt takes over the reference release gave up, opens the image in place, and checks its seal. The document is read where it is and dropped, which returns its block to the pool. One image of the example was damaged after it was sealed: adopt refuses it with errc::damaged_document, and the reference it was handed is still counted, so the consumer gives it back by offset with pool.drop(offset), which reads nothing of the image and returns the block:

// The consumer: it adopts each offset the producer sends, prints the document and drops it, so
// that its block returns to the pool; an offset the seal refuses is dropped by the pool, unread.
int consume(const char* segment, const char* queue_name) {
    const bip::shared_memory_object memory(bip::open_only, segment, bip::read_write);
    const bip::mapped_region mapped(memory, bip::read_write);
    const std::span<std::byte> bytes(static_cast<std::byte*>(mapped.get_address()),
                                     mapped.get_size());
    const wd::result<wd::shared_pool> attached = wd::shared_pool::attach(bytes);
    if (attached.has_error()) {
        std::cout << "error: " << attached.error().message() << '\n';
        return 1;
    }
    const wd::shared_pool pool = *attached;
    bip::message_queue queue(bip::open_only, queue_name);

    for (;;) {
        std::uint64_t offset = 0;
        std::size_t received = 0;
        unsigned int priority = 0;
        queue.receive(&offset, sizeof offset, received, priority);
        if (offset == end_of_stream) {
            return 0;
        }

        wd::result<wd::shared_document<>> adopted = wd::shared_document<>::adopt(pool, offset);
        if (adopted.has_value()) {
            std::cout << adopted->get().stringify() << ": use count " << adopted->use_count()
                      << '\n';
        } else {
            std::cout << "refused: " << adopted.error().message() << '\n';
            // A refused reference stays counted until it is given back: the pool drops it by its
            // offset, without reading the image, and the block returns to the pool.
            if (const wd::result<void> dropped = pool.drop(offset); dropped.has_error()) {
                std::cout << "error: " << dropped.error().message() << '\n';
                return 1;
            }
        }
    }
}
available before the consumer: 4
{"name":"Ada","born":1815,"langs":["Analytical"]}: use count 1
{"name":"Grace","langs":["COBOL"]}: use count 1
{"name":"Alan","n":1500}: use count 1
refused: a sealed image whose bytes do not match its seal
available after the consumer: 8

Before the consumer ran, the pool had four free blocks of the eight, the four documents’ blocks being leased; after it, all eight are free. The reference count of each document read was one, the consumer’s.

A process that dies holding references leaves their blocks counted for good, as Boost.Interprocess’s own shared_ptr does; recovering them is the program’s. Images in shared memory are trusted input, as every image is (Sealed images): the processes that map a pool trust one another.

Compared with Boost.JSON

Boost.JSON is the JSON library of Boost, and document reads JSON with its parser. Its document, boost::json::value, is built for speed, and document is built to behave as JavaScript does and to move as bytes: the two make different choices, and this section puts them side by side. Each part of the program below runs the same JSON through both libraries and prints both outputs. The line of document is what JavaScript gives where the part says nothing else, and the line of Boost.JSON is what that library gives. It uses Boost.JSON by <boost/json.hpp>, beside document’s header:

#include <webcpp/document.hpp>

#include <boost/json.hpp>

#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <span>
#include <string>
#include <string_view>

namespace wd = webcpp::document;
namespace json = boost::json;
/** Prints the document's line of a section, under the label that names it. */
void print_document(std::string_view text) {
    std::cout << "  document:   " << text << '\n';
}

/** Prints the Boost.JSON line of a section, under the label that names it. */
void print_boost(std::string_view text) {
    std::cout << "  Boost.JSON: " << text << '\n';
}

/** The text Boost.JSON writes for `text` once parsed, or the error it reports without throwing. */
std::string boost_text(std::string_view text) {
    boost::system::error_code ec;
    const json::value v = json::parse(text, ec);
    return ec ? ec.message() : json::serialize(v);
}

Key order

boost::json::object keeps the keys in the order they came. JavaScript puts the keys that are array indices first, ascending, then the others in the order they were created, and document does the same (Key order):

constexpr std::string_view text = R"({"b":1,"2":2,"a":3,"1":4})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
    return false;
}
std::cout << "1. Key order\n";
print_document(made->stringify());
print_boost(boost_text(text));
1. Key order
  document:   {"1":4,"2":2,"b":1,"a":3}
  Boost.JSON: {"b":1,"2":2,"a":3,"1":4}

A key that is repeated keeps the last value, in the place of the first, as JavaScript does:

constexpr std::string_view text = R"({"b":1,"2":2,"b":3,"1":4})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
    return false;
}
std::cout << "6. Repeated keys\n";
print_document(made->stringify());
print_boost(boost_text(text));
6. Repeated keys
  document:   {"1":4,"2":2,"b":3}
  Boost.JSON: {"b":3,"2":2,"1":4}

Erasing keeps the order

boost::json::object::erase copies the last member into the gap, so d comes before b and c. JavaScript’s delete keeps the order of the others, and so does document’s erase:

constexpr std::string_view text = R"({"a":1,"b":2,"c":3,"d":4})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error() || !made->root().erase("a")) {
    return false;
}
std::cout << "2. Erase keeps the order of the other members\n";
print_document(made->stringify());

// Boost.JSON's erase copies the last member into the gap, so d comes before b and c.
boost::system::error_code ec;
json::value v = json::parse(text, ec);
json::object* object = v.if_object();
if (ec || object == nullptr) {
    return false;
}
object->erase("a");
print_boost(json::serialize(v));
2. Erase keeps the order of the other members
  document:   {"b":2,"c":3,"d":4}
  Boost.JSON: {"d":4,"b":2,"c":3}

Exact numbers

Boost.JSON stores a number as a double when it is not an integer, and writes it back as 1E-1. document keeps the digits that were sent, and writes them as JavaScript writes a number (Numbers):

constexpr std::string_view text =
    R"([0.1000000000000000000000000000000001,12345678901234567890123])";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
    return false;
}
// The digits are the ones read; 23 of them are written in JavaScript's exponent form.
std::cout << "3. Exact numbers\n";
print_document(made->stringify());
print_boost(boost_text(text));
3. Exact numbers
  document:   [0.1000000000000000000000000000000001,1.2345678901234567890123e+22]
  Boost.JSON: [1E-1,1.2345678901234568E22]

Array methods

boost::json::array has insert and erase, which make the same edit as splice and unshift but return no removed values. document has the array methods of JavaScript (Arrays):

wd::result<wd::document> made = wd::parse("[1,2,3,4]");
if (made.has_error()) {
    return false;
}
wd::value a = made->root();
const wd::result<std::uint64_t> length = a.unshift(0);
// splice removes two elements from index 1, returns them, and inserts "x" in their place.
const wd::result<wd::document> removed = a.splice(1, 2, "x");
if (length.has_error() || removed.has_error()) {
    return false;
}
std::cout << "4. Array methods\n";
print_document("unshift(0) -> " + std::to_string(*length) + "; splice(1, 2, \"x\") removes " +
               removed->stringify() + "; a = " + a.stringify());

// Boost.JSON's array has insert and erase, which make the same edit but return no removed
// values.
json::array arr{1, 2, 3, 4};
arr.insert(arr.begin(), 0);
arr.erase(arr.begin() + 1, arr.begin() + 3);
arr.insert(arr.begin() + 1, json::value("x"));
print_boost(
    "no splice or unshift; insert and erase make the edit, without the removed "
    "values: a = " +
    json::serialize(arr));
4. Array methods
  document:   unshift(0) -> 5; splice(1, 2, "x") removes [1,2]; a = [0,"x",3,4]
  Boost.JSON: no splice or unshift; insert and erase make the edit, without the removed values: a = [0,"x",3,4]

An image copied and opened in place

A boost::json::value points into its memory resource, so its bytes cannot be copied to another buffer: the only way to move it is to serialize it and parse it again. document’s image is the document, and a copy of its bytes in a buffer of the program’s own, opened there, is the same document with no parse (Images):

constexpr std::string_view text = R"({"user":"Ada","langs":["Analytical","Note G"]})";
wd::result<wd::document> made = wd::parse(text);
if (made.has_error()) {
    return false;
}
const std::span<const std::byte> image = made->image();
// A buffer of the program's, 16-aligned, large enough for the image.
alignas(16) std::array<std::byte, 8192> buffer{};
if (image.size() > buffer.size()) {
    return false;
}
std::ranges::copy(image, buffer.begin());
// Opened in place: the buffer becomes the document, with no parse.
wd::result<wd::basic_document<wd::default_traits, wd::external>> opened =
    wd::open(std::span<std::byte>(buffer.data(), image.size()));
if (opened.has_error()) {
    return false;
}
std::cout << "5. An image copied to another buffer and opened in place\n";
print_document(opened->stringify());
// A Boost.JSON value points into its memory resource, so its bytes cannot be copied: the
// only way to move it is to serialize it and parse it again.
print_boost(boost_text(made->stringify()));
5. An image copied to another buffer and opened in place
  document:   {"user":"Ada","langs":["Analytical","Note G"]}
  Boost.JSON: {"user":"Ada","langs":["Analytical","Note G"]}

Errors are values

document’s operations return a result, neither a bad parse nor a bad write throws, and the core never throws at all. Boost.JSON’s parse reports through an error_code when it is given one, but its accessors, at and as_string among them, throw (Errors):

// The document's operations return result; neither a bad parse nor a bad write throws.
const wd::result<wd::document> broken = wd::parse("[1,");
wd::result<wd::document> number = wd::parse("5");
if (broken.has_value() || number.has_error()) {
    return false;
}
const wd::result<std::uint64_t> pushed = number->root().push(1);
std::cout << "7. Errors are values\n";
print_document("parse(\"[1,\") -> " + broken.error().message() + "; push on a number -> " +
               pushed.error().message());

// Boost.JSON's parse reports through error_code, but its value accessors (at, as_string)
// throw, so this section stays with the error_code overload.
boost::system::error_code ec;
static_cast<void>(json::parse("[1,", ec));
print_boost("parse(\"[1,\", ec) -> " + ec.message());
7. Errors are values
  document:   parse("[1,") -> malformed JSON text; push on a number -> a value of another kind
  Boost.JSON: parse("[1,", ec) -> incomplete JSON

What Boost.JSON does better

document is not a fast JSON tree. Boost.JSON parses faster, stringifies faster, finds a key faster (it hashes, where document walks an ordered tree) and splices an array faster, and its values are smaller than document’s images: a node of an image takes 192 bytes whatever it holds (Memory per value). The benchmarks measure each of these against Boost.JSON and against JavaScript (Benchmarks, and Reading the results for where document is slower and why). The reasons to choose document are the ones above, and the ones that no parser has: an image that is sent as it is, opened in place and shared between threads and processes, which Images on the wire and Sharing teach.

The sugar that throws

document has two layers. The core, every function this page has shown so far, never throws and exists in every build: each operation that can fail returns a result. The sugar reads as JavaScript reads, and throws as JavaScript throws, so it exists only in a build with exceptions:

Sugar Its core function

h = x

h.set(x)

h = other, doc["a"] = doc["b"]

h.set(other): a deep copy of the other handle’s value

h.as<T>()

h.get<T>()

h.as_string()

h.get<std::string_view>()

h.as_bool()

h.get<bool>()

wd::parse_or_throw(text)

wd::parse(text)

wd::document d;

wd::document::create()

wd::document copy = doc;

doc.clone()

Each member is its core function, then, on failure, the exception of the core’s error: errc::syntax throws wd::syntax_error, which also holds the byte offset; errc::type throws wd::type_error, errc::range wd::range_error, errc::capacity wd::capacity_error, and errc::not_a_document and errc::incompatible_document wd::image_error. Each derives from wd::error, which derives from std::runtime_error and holds the code (Errors). A member that throws has changed nothing, since its core function completes or changes nothing.

wd::document doc = wd::parse_or_throw(R"({"n":1,"user":{"name":"Ada"}})");
doc["n"] = 2;                      // doc.n = 2
doc["user"]["nick"] = "Countess";  // creates the member
const auto n = doc["n"].as<std::int64_t>();
const std::string_view name = doc["user"]["name"].as_string();
std::cout << n << ' ' << name << '\n';

// Assignment writes at the handle's place, a deep copy, and never rebinds the handle.
wd::value place = doc["n"];
place = doc["user"]["name"];
show("doc.n", doc["n"]);
doc["user"]["nick"] = wd::undefined;  // erases the member
show("doc.user", doc["user"]);

try {
    doc["n"]["x"]["y"] = 1;  // a write through nothing
} catch (const wd::type_error& e) {
    std::cout << "TypeError: " << e.what() << '\n';
}
try {
    static_cast<void>(doc["user"].as<bool>());
} catch (const wd::type_error& e) {
    std::cout << "TypeError: " << e.what() << '\n';
}
try {
    static_cast<void>(wd::parse_or_throw("[1,]"));
} catch (const wd::syntax_error& e) {
    std::cout << "SyntaxError: " << e.what() << " at " << e.offset() << '\n';
}

const wd::document empty;  // holds null
wd::document twin = doc;   // an equal and independent copy
twin["n"] = 3;
std::cout << empty.stringify() << ' ' << doc.stringify() << ' ' << twin.stringify() << '\n';
2 Ada
doc.n -> "Ada"
doc.user -> {"name":"Ada"}
TypeError: a value of another kind
TypeError: a value of another kind
SyntaxError: malformed JSON text at 3
null {"n":"Ada","user":{"name":"Ada"}} {"n":3,"user":{"name":"Ada"}}

Its rules

  • Assignment writes; it never rebinds. h = x stores x at the place h names, and h still names that place afterward. h = other, with other a handle, stores a deep copy of the value other names, as doc["a"] = doc["b"] does: never an alias (Copies, never aliases).

  • = wd::undefined erases a member, as set(wd::undefined) does.

  • as_string() reads UTF-8 strings only: it returns a std::string_view, which a UTF-16 string cannot give. Under js_traits, a program uses as<std::u16string_view>() or as<std::string>().

  • wd::document d; holds null, and the copy constructor gives an equal and independent copy. Both are for owned documents only: an external document has no buffer to be made or copied into.

  • An external document throws wd::capacity_error when it is full, where set would return errc::capacity.

Without exceptions

In a build without exceptions, the sugar is not there: each member carries the constraint detail::exceptions_enabled, which no such build satisfies, so a call does not compile, and the compiler’s note names the constraint. For as<std::int32_t>(), Clang writes "no matching member function for call to 'as'", with the note "because 'int' does not satisfy 'exceptions_enabled'", and the test no_exceptions holds what it writes for each member (Testing). A generic program can ask whether the sugar is there:

/** Whether a handle of type `H` has the sugar `as<T>()`, which throws. */
template <class H>
concept has_as = requires(const H& h) { h.template as<std::int64_t>(); };

/** Whether a handle of type `H` can be assigned a value, `h = 1`, which throws on failure. */
template <class H>
concept assignable = requires(H h) { h = 1; };

static_assert(!has_as<wd::const_value>, "as<T>() needs exceptions");
static_assert(!assignable<wd::value>, "h = x needs exceptions");
static_assert(!std::is_default_constructible_v<wd::document>, "wd::document d; needs exceptions");
static_assert(!std::is_copy_constructible_v<wd::document>, "copying a document needs exceptions");

The core is the same in both builds, and an example of it is in Errors. A build has no exceptions when webcpp has none, or when the program defines WEBCPP_DOCUMENT_NO_EXCEPTIONS for document alone (Without exceptions).

Errors

Every operation of the core that can fail returns a wd::result<T>, Boost.System’s result: the value, or a boost::system::error_code. document’s own codes are the enumerators of wd::errc, of the category webcpp.document, whose values are fixed and never reused, so that a stored code keeps its meaning across versions. The error a sink or a selector’s callback returns is passed on unchanged, in its own category.

errc When JavaScript Exception, with exceptions only

syntax, 1

malformed JSON text, with Boost.JSON’s code as the parser’s cause() and the byte offset as its offset(); text after a finished parse

SyntaxError

wd::syntax_error, with offset()

type, 2

a value of another kind; a write through nothing; text invalid in the encoding; a malformed selector; a method of arrays on anything but an array

TypeError

wd::type_error

range, 3

a number that does not fit or is not exact; a date out of JavaScript’s range

RangeError

wd::range_error

capacity, 4

no room: a full external buffer, an owned document at its max_bytes, a parser’s limit

none; RangeError from the module

wd::capacity_error

not_a_document, 5

bytes that are not an image

none; TypeError from the module

wd::image_error

incompatible_document, 6

an image of another format version, reserved bytes, layout, byte order or traits

none; TypeError from the module

wd::image_error

damaged_document, 7

a sealed image whose bytes do not match its seal: changed, torn or cut short (Sealed images)

none; TypeError from the module

wd::image_error

The JavaScript column is the error JavaScript itself throws for the same fault; the module of Errors throws a SyntaxError, a RangeError or a TypeError for every code.

An errc converts to an error_code and compares with one, r.error() == wd::errc::type, and wd::error_category() is the category. The program below is built without exceptions, which example/Jamfile asks for it whatever the build asks, and makes each error once:

/** Prints what failed, its code's value and category, and its message. */
void print(std::string_view what, boost::system::error_code e) {
    std::cout << what << ": " << e.category().name() << ':' << e.value() << ", " << e.message()
              << '\n';
}
// errc::syntax: malformed text, and the parser's offset where it was found.
wd::parser p;
const wd::result<void> written = p.write("[1,]");
print("[1,]", written.error());
std::cout << "  at byte " << p.offset() << ", Boost.JSON's cause: " << p.cause().message()
          << '\n';

wd::result<wd::document> made = wd::parse(R"({"n":300})");
if (made.has_error()) {
    return 1;
}
wd::document& doc = *made;
// errc::type: a write through nothing, or a read of another kind.
print("doc.n.x.y = 1", doc["n"]["x"]["y"].set(1).error());
print("doc.n as a string", doc["n"].get<std::string>().error());
// errc::range: a number that does not fit, or a date out of JavaScript's range.
print("doc.n as int8_t", doc["n"].get<std::int8_t>().error());
// errc::capacity: a limit, here of depth.
wd::parse_options shallow;
shallow.max_depth = 1;
print("[[1]], max_depth 1", wd::parse("[[1]]", shallow).error());
// errc::not_a_document and errc::incompatible_document: bytes that are no image of these
// traits.
print("64 zero bytes", wd::from_image(std::array<std::byte, 64>{}).error());
print("another traits' image", wd::from_image<wd::js_traits>(doc.image()).error());
[1,]: webcpp.document:1, malformed JSON text
  at byte 3, Boost.JSON's cause: syntax error
doc.n.x.y = 1: webcpp.document:2, a value of another kind
doc.n as a string: webcpp.document:2, a value of another kind
doc.n as int8_t: webcpp.document:3, a number or index out of range
[[1]], max_depth 1: webcpp.document:4, no room left
64 zero bytes: webcpp.document:5, not a document's image
another traits' image: webcpp.document:6, an image of another format, layout, byte order or traits

An operation that fails changes nothing, so the program goes on with the document as it was:

// An error compares with an errc, and the document is as it was before the call that failed.
if (const wd::result<void> set = doc["n"]["x"].set(1); set.error() == wd::errc::type) {
    std::cout << "still " << doc.stringify() << '\n';
}
still {"n":300}

A failure of the process heap is not an error value. The image’s heap is never exhausted by surprise, since every operation finds its room first, but a few things live on the process heap: the text stringify returns, the key a handle of a missing member holds, and the parser’s frames. The standard library’s std::bad_alloc there is thrown with exceptions and ends the program without them, as it does for any C++ container.

Testing

A test of a program that uses document compares values: a document with ==, which compares structurally, or the text stringify writes, which is what JSON.stringify writes, byte for byte, so that an expected text can be measured with Node. Every failure is a value, which a test compares with an errc; nothing depends on a clock, a thread or global state; and the same operations give the same image, so that two runs, or two targets, can be compared byte for byte.

How document is tested

document’s tests are lightweight_test programs, one per subject, under test/, each declared in test/Jamfile with webcpp’s webcpp.run. The JavaScript facts they hold are Node 26’s:

Test What it checks

errors

The values of errc and kind, which are fixed, the category’s name and messages, and the conversion and the comparison with an error_code; built again with exceptions as errors_raise, the exception each errc raises.

number

Numbers read from JSON text exactly, at the edges of their text and of decimal128_t; written as JavaScript’s Number::toString writes them, row by row of Numbers; converted to and from every integer width and decimal type.

traits

Array-index keys; dates as Date.prototype.toJSON writes them; base64 against RFC 4648’s vectors; UTF-8 and UTF-16 validation and transcoding; the scalar and number codecs; the concept traits satisfy, and the tags that tell traits apart.

heap

The image’s heap: strings of exact size zeroed when freed, an allocator that hands out and frees zeros, probes that leave the heap as it was, regions owned and external, grown, copied and shrunk, the same bytes at any address, and the header’s check.

selector_grammar

The selectors’ grammar and matcher, apart from any parser: which texts parse into which steps, which are refused, and which paths each selector matches.

tree

The tree under each object policy, keys that look like array indices, elements renumbered, deep copies and literals, the header it keeps, and every operation, which completes or changes nothing, in buffers of every size and in owned images that grow.

literal

wd::object and wd::array before a document stores them: each form a value takes, nesting, members in the order written, and what a literal refuses at compile time.

convert

Every settable type stored by value, and every node read back as every readable type, with their type and range failures; text validated and transcoded; dates, bytes, and structural equality.

value

The handles: navigation that never fails, set that creates and replaces, numbers, strings, dates and bytes, value_or, erase and assign_from, deep copies, a handle kept while the image grows and moves, equality, iteration, a deep copy into external buffers of every size, a moved-from document and one that holds undefined.

order

The key order through the handles, with keys that look like array indices and one that holds a zero byte, under each object policy and both encodings, sorted keys by UTF-16 code units with either: assignment keeps a key’s place, and a key erased and created again takes a new key’s.

image

Images copied to another address and opened there, formatted, refused, the same for the same operations, no byte left of an erased secret, shrunk, a full external buffer, a buffer that is not 16-aligned, and a header whose container offset lies.

stringify

stringify and stringify_to against Node’s text: indentation, empty containers, escapes, numbers, dates, bytes under each policy, a lone surrogate under js_traits, an undefined root, every piece size from 1 to 64, a sink that fails or throws, and a document nested 100,000 levels deep.

parse

What parse accepts, written back as Node writes it, and what it refuses, with the offset; the limits of depth, bytes and image, exact; an object of 100,000 keys and a 64 MB string; the revivers; a parse into external buffers of every size. Built again with exceptions as parse_sugar, it checks parse_or_throw.

streaming

Every text of the corpus split at every byte, and written one byte at a time, gives the document a whole parse gives; a syntax error’s offset is the same however the text is split; a failed parser stays failed and a finished one takes no more text, nor a second text after its first; max_bytes counts every chunk; an external parser.

selector

Deliveries by on and observe, nested values first, scalars when their token ends, the root, a callback that fails or throws, a function as a callback, a callback that cannot write, a million elements under on within 64 KB of image, the same deliveries however the text is split, and paths that parse back as selectors.

stream

Streams of documents: each framing on its own rows, NDJSON’s rules one by one, indented records and bad ones in JSON text sequences, errors skipped with their lines or stopping the stream until a reset, the limits held per document, and every stream split at every byte and written one byte at a time giving what one write gives, through next() and through the push form alike.

stream_asio

Natively only: async_read_document over a loopback TCP connection, the writer sending NDJSON and concatenated JSON in odd-sized pieces, read with a callback and, with exceptions, in a coroutine with asio::use_awaitable; a skipped line reported with its line, eof at the end, and a completion that needs no read made after the call returns.

array

Each array method against Node’s results, with JavaScript’s index rules, on empty arrays and on anything but an array; a handle kept while pushes grow the image; and a splice into external buffers of every size, which completes or changes nothing.

sugar

Built with exceptions: assignment stores as set does and never rebinds, so std::swap of two handles writes through them, as, as_string and as_bool read as get does, a default-constructed document holds null, a copy is equal and independent, and each failure throws the exception of the core’s code and changes nothing.

no_exceptions

Built without exceptions, a test of the compiler’s diagnostics: each member of the sugar called once does not compile, and the note names exceptions_enabled (The sugar that throws).

oracle_cases

Node as the oracle, from C++: every case of the corpus parsed, changed by its operations through the core and written, under default_traits and js_traits, must give Node’s text and Node’s return values, byte for byte (Node as the oracle).

Each case is a function named after the rule it checks, such as handle_survives_growth, and main calls them all and returns boost::report_errors(). Every test is built and passes without exceptions too, but those that declare <exception-handling>on. Beside them, test/Jamfile compiles each public header alone, as the only include of its translation unit, so that a header that leans on an include it does not make fails, naming it. And example/Jamfile builds every program this page shows, runs it, and compares what it printed with the .expected beside it; a program that returns anything but 0 fails too.

Node as the oracle

test/oracle/ runs the same cases in Node and in C++. A case, test/fixtures/cases/<name>.json, is a JSON text, an indent, and a list of operations: set, erase, push, pop, shift, unshift, splice, reverse, the length set and Object.assign, each value written as JSON text, so that neither side reads a number through a double. oracle.mjs runs each case with JSON.parse, the operations in JavaScript and JSON.stringify, and writes what it gets to test/fixtures/expected/<name>.json, which is committed; the test oracle_cases runs each case through the document and must give those bytes. The cases written by hand follow the rules of this page, key order, numbers, arrays, strings, dates, bytes and indentation; the others are random documents and operations, which generate.mjs writes from a fixed seed. A case of numbers a double does not keep runs with js_traits alone; every other runs with both traits.

No case holds what the two sides do differently by design (Where document differs, on purpose): a lone surrogate, a number past a double’s range or past 34 digits, an operation JavaScript would throw on, or the key __proto__, which sets a prototype in JavaScript.

Without exceptions

That document works without exceptions is checked by the lint, which compiles every public header, and every test and example, which instantiate its templates, with exception-handling=off. <webcpp/document/config.hpp>, which every public header of document includes first, defines WEBCPP_DOCUMENT_NO_EXCEPTIONS when webcpp has no exceptions (WEBCPP_HAS_EXCEPTIONS is not defined: the compiler has none, or the program defined WEBCPP_NO_EXCEPTIONS), and a program may define it for document alone, as Boost.Asio’s BOOST_ASIO_NO_EXCEPTIONS does. The core never throws, so the macro removes only the sugar that throws and the exception types (The sugar that throws). The tests config, config_developer_defined and config_all_off check the three ways it is defined, and the lint refuses a throw in a public header: an exception goes through boost::throw_exception.

Targets

test/Jamfile and example/Jamfile declare the targets they are built for, webcpp.targets native emscripten wasip2 wasip3, and b2 builds each program for those alone. From the root of a webcpp checkout, each of these builds and runs:

b2 libs/document/test

The 63 tests natively: the lightweight_test programs, and each of the 28 public headers compiled alone. stream_asio, and asio.hpp compiled alone, are built natively only, as Boost.Asio is (With Boost.Asio).

b2 libs/document/example

The 20 examples, each built, run, and its output compared with the one beside it.

b2 libs/document/test libs/document/example toolset=emscripten

The same on emscripten: 65 tests and 13 examples, each run by node, b2’s toolset itself, except the 6 driven programs, the tests of the JavaScript API and the example of Fed from JavaScript, which the own lane js runs.

b2 libs/document/test/js//js toolset=emscripten

The own lane js (Using it from JavaScript): Node’s test runner on the module and on the module of shared documents, the npm package as a user receives it, and the two examples in headless Chromium, and Node over the example that binds a stream parser (example/document_stream_js.cpp). Each test builds the module it checks, so the lint and the CI compile it.

b2 libs/document/test libs/document/example toolset=clang-wasip2 testing.launcher=wasmtime

The same for wasm32-wasip2: 59 tests and 14 examples, each run by wasmtime. toolset=clang-wasip3 does the same for wasm32-wasip3: 59 tests and 14 examples.

b2 libs/document/test/oracle//oracle

The oracle lane: Node runs every case, and what it writes must be what test/fixtures/expected holds. It needs Node, and no package.

b2 libs/document/test/oracle//update-expected

Not a check: it writes Node’s results again, over the committed ones, after a case is added or changed.

b2 compares timestamps, so a result that counts is built from scratch, with b2 -a. b2 libs/document/test//value builds and runs one test alone, while working on it.

The test matrix

The continuous integration runs document’s tests and examples in lanes, one toolset each: the native compilers, emscripten, wasip2 and wasip3. Each lane builds from scratch and records what b2 built and ran:

b2 -a --dump-tests --out-xml=<lane>.xml toolset=<toolset> libs/document/test libs/document/example

webcpp’s tools/report/report.py merges the files of every lane into a matrix, published with the documentation at https://webcpporg.github.io/report/.

Benchmarks

document does what JavaScript does, and does it in C++: this section measures what that costs. Each operation is timed three ways: with Boost.JSON, the C++ library whose parser document uses, with document itself, and with JavaScript’s own objects in V8, the engine whose behaviour document reproduces. The C++ runs natively, and compiled to WebAssembly with Emscripten in the same Node.js and the same headless Chrome as the JavaScript, so that each row compares the three where a web application would run them.

The operations of the survey and the data come from recognised benchmarks, never from document’s own tests; the operations of binary transmission, which no suite times, come from the case document is built for, and are the first results below. Every implementation must give the same answer to each before it is timed. One C++ program, bench/bench.cpp, holds the Boost.JSON and document sides; one JavaScript module, bench/bench.mjs, holds JavaScript’s and the same harness, which Node runs and Chrome runs in a page; and bench/run.mjs runs them all, checks their answers and writes the results below.

The operations

The fourteen operations of the survey of JSON and dictionary benchmarks that this section follows, each with the suites it is taken from. No recognised JSON suite times changes to a document (insert, update, delete, splice) or deep copies: those come from the hash-map suites, V8’s own js-perf-test and Dromaeo’s DOM tests. Equality, the survey’s fifteenth, is left out, since no suite times it. Three more, 15 to 17, are binary transmission: a document sent to another place and opened there. No suite times them, so the page takes them from the case the image is built for.

# Operation Taken from Boost.JSON document JavaScript

1

Parse

nativejson-benchmark, Boost.JSON’s bench, serde’s json-benchmark, nlohmann’s benchmarks, JetStream 2.1

parse

parse

JSON.parse

2

Stringify, compact

nativejson-benchmark, Boost.JSON’s bench, nlohmann, simdjson, JetStream 2.1

serialize

stringify()

JSON.stringify(v)

3

Prettify, indent 4

nativejson-benchmark, nlohmann, simdjson

* by hand over its serializer, as its example pretty.cpp writes one

stringify(4)

JSON.stringify(v, null, 4)

4

Full traversal (Statistics)

nativejson-benchmark: its Stat counts of objects, arrays, numbers, strings, true, false, null, members, elements and string lengths

a walk by kind()

a walk by kind(), entries() and the array’s range

a walk by typeof and for (k in o)

5

Key-path queries over twitter.json, parse excluded

simdjson: find_tweet, top_tweet, distinct_user_id

at and as_int64, as simdjson’s boostjson.h

handles and value_or

property access

6

Lookup, hit and miss

Tessil’s hash-map benchmark, martinus’s map benchmark, V8’s PropertyQueries, abseil

if_contains; contains

o[key]; contains

o[key]; key in o

7

Insert a member

Tessil, abseil’s TryEmplace, V8’s Map-Set

insert_or_assign

o[key].set(i)

o[key] = i

8

Update a member

abseil’s InsertOrAssign, martinus

insert_or_assign

o[key].set(v)

o[key] = v

9

Delete members, then look every key up

Tessil (reads after deleting), martinus, V8’s Map-Delete, Dromaeo’s removeChild

* stable_erase, which keeps the order as delete does

erase

delete o[key]

10

Iterate entries

Tessil, martinus, abseil, V8’s Keys and Object, Dromaeo’s traverse

a range for

entries()

for (k in o) and o[k]

11

Array push and pop

Dromaeo’s appendChild and removeChild, martinus

push_back, pop_back

push, pop

push, pop

12

Array splice in the middle

Dromaeo’s insertBefore and removeChild

* insert, then erase

splice

splice

13

Array methods: map, filter, reduce, find, slice, join

V8’s js-perf-test Array

* loops by hand; slice by the array’s range constructor

* loops by hand; slice

the methods; * join after a map of the ids

14

Deep copy

martinus’s Copy, abseil’s CopyCtor, Dromaeo’s cloneNode

the copy constructor

clone

structuredClone, and JSON.parse(JSON.stringify(v)) apart

15

Send and receive a document

none: binary transmission, which document is built for

serialize, then parse

the image copied into a buffer of the receiver’s, then open in place

JSON.stringify, then JSON.parse

16

Send and receive a sealed document

none: the same, with the image’s seal checked

-

seal(), the copy, then open, which checks the seal first

-

17

Pass a document to a Worker, which answers each pass

none: the Worker of a web application

-

the image copied into a transferable ArrayBuffer, transferred, copied into the Worker’s memory and opened there in place

postMessage of the object, which clones it

A * marks the closest form of an operation an implementation has no direct form of, as the survey names it; the results’ tables carry the same mark. Rows 15 to 17 have no *: each implementation has its own form of sending a document, and a - marks one that has none. Every array method of document but slice is a loop by hand: document has the methods that change an array (push, pop, splice and the others) and slice and concat, not the callbacks of map or filter. A loop that builds an array copies each element it keeps into a new document, where JavaScript’s map, filter and slice share the elements they keep with the array they read.

The data

  • nativejson-benchmark’s three files (Milo Yip’s, MIT): canada.json, 2.2 MB of GeoJSON, mostly numbers; citm_catalog.json, 1.7 MB of wide objects; twitter.json, 0.6 MB of strings, many of them Japanese, and escapes. The repository is a submodule pinned at 478d572, third_party/libs/nativejson-benchmark, which /webcpp/third_party//nativejson-benchmark gives a program: the files are read where the checkout holds them, never copied.

  • For the arrays, the largest ring of the polygon of canada.json, an array of 14,310 points, each an array of two numbers, and the 100 statuses of twitter.json.

  • For the dictionaries, Tessil’s synthetic objects: 10, 1,000 and 100,000 members whose keys are 15 or 50 characters long, a letter then letters and digits, so that none is an array index, each member’s value its index. The keys come from Mulberry32 with a fixed seed per object, in C++ and in JavaScript alike, bit for bit; the lookups take them in a shuffled order, from a seed of their own; the misses are as many keys of the same length from another seed. Beside them, the widest object of citm_catalog.json, events, of 184 members, whose keys are nine-digit integers: array indices to JavaScript, which keeps such keys in ascending order, as document does and as the file lists them.

  • A pass of operation 9 deletes every other key of the lookups’ order, at most 1,000 of them, then looks every key up: Boost.JSON’s stable_erase moves every member after the one it erases, and deleting half of 100,000 members takes it 25 s a pass.

The sizes on the wire

What a document weighs when it is sent is measured apart from its time, and not timed. Each file’s value is sent three ways: as the JSON text JSON.stringify writes, as the text Boost.JSON’s serializer writes, and as document’s image after shrink_to_fit. Each is shown as it is, gzipped and brotli-compressed with Node’s zlib at its default levels (gzip’s level 6, brotli’s quality 11). The sizes are in bytes:

File Form Bytes gzip brotli

canada.json

JSON text, JSON.stringify

2,090,234

551,870

427,666

canada.json

JSON text, Boost.JSON serialize

2,285,629

555,391

439,584

canada.json

document’s image

32,099,376

2,432,828

1,345,771

citm_catalog.json

JSON text, JSON.stringify

500,299

17,024

7,848

citm_catalog.json

JSON text, Boost.JSON serialize

500,299

17,024

7,848

citm_catalog.json

document’s image

8,531,152

617,425

186,897

twitter.json

JSON text, JSON.stringify

466,906

44,864

31,891

twitter.json

JSON text, Boost.JSON serialize

466,907

44,866

31,902

twitter.json

document’s image

3,652,288

420,181

146,804

The image is the larger on the wire in every row. It holds a node of a tree for each value, with its ids and offsets, where the text holds the value’s characters alone, so it is 8 to 17 times the text’s size uncompressed; compressed it is still 3 to 9 times larger for canada.json and twitter.json, and for citm_catalog.json the gzip and brotli of the image are 36 and 24 times the text’s. The sizes come from bench/wire.mjs, which reads the files that bench-native --wire <dir> writes; the results target does not write them.

A seal is a CRC-32C of the image’s bytes, which catches accidental damage (a flipped bit, a torn write). It is not authentication: anyone can compute a valid seal for any bytes, so an image from an untrusted source must not be opened, sealed or not.

Methodology

The survey’s section 4, which gathers the practice of the suites above:

  • Loading is never timed: the files are read, and the documents an operation reads are parsed and built, before its first pass. An operation that changes its input, such as delete, gets fresh copies, made before a sample and outside its time.

  • The first pass of each operation is timed alone, before any warm-up: the first-iteration time, as JetStream reports it, which tells how the code runs before the JIT or the caches have seen it. Its answer is then checked against every other implementation’s: the Stat counts of nativejson-benchmark for a parse, a stringify read back, a traversal, a copy or a document sent and received (sealed or not, and in a Worker, the Worker’s statistics of what it received); the queries’ answers; the members found and the sums of their values; lengths and checksums for the arrays. A run whose implementations disagree writes no results.

  • The warm-up doubles the passes of a sample until it lasts at least 10 ms, then runs one more sample, which is discarded. The result is the median of 10 samples of at least 10 ms, divided by the passes and by the operations of a pass.

  • JavaScript runs with --expose-gc (in Chrome, --js-flags=--expose-gc), and gc() runs before each sample; its passes add their results to a checksum, which the run prints, and the C++ passes write theirs to a volatile object, so that no compiler drops the work.

  • Chrome loads its page from a server of the run, on 127.0.0.1, which makes it cross-origin isolated (COOP and COEP headers): its performance.now then reads 5 us rather than 100 us. Node and Chrome share V8’s major version, 14: Node 26 and the Chrome for Testing 147 headless shell that Playwright 1.59 installs, which bench/package-lock.json pins.

  • Each implementation runs in a process of its own, or in a page of its own. The C++ is b2’s release variant: -O3 natively, and -O3 for Emscripten too, at compilation and at link.

  • document runs under default_traits: exact decimal numbers and UTF-8 text. Parse and stringify also run under js_traits, binary doubles and UTF-16, which JavaScript’s own representation is, in rows of their own.

  • A C++ pass frees the result of the pass before it, so its time includes freeing it, as Boost.JSON’s own benchmark does; JavaScript’s garbage is collected when V8 chooses, and gc() runs between samples.

The machine and the results

b2 libs/document/bench//results builds the programs, measures everything, and writes the results below, with the machine and every version they were measured with; run again, on another machine, it measures that one.

Measured on 2026-10-12, on this machine:

Version

Machine

Apple M5 Max, 18 cores, 128 GB

Operating system

macOS 27.0 (26A428), arm64

Native compiler

Apple clang 21.0.0 (clang-2100.3.34.2), release variant (-O3)

Boost

1.92

Emscripten

6.0.11 (clang 24.0.0git), release variant, linked with -O3

Node.js

v26.7.0, V8 14.6.202.34-node.28

Chrome

Chrome 147.0.7727.15, Playwright’s headless shell, V8 14.7.173.6

Each cell is the median time of one operation, and below it, in italics, the time of the first one, before any warm-up. A * marks the closest form of an operation that the implementation has no direct form of (The operations); a -, an implementation that the row does not concern.

1. Parse

From nativejson-benchmark, Boost.JSON bench, serde json-benchmark, nlohmann, JetStream 2.1; time per document.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

canada.json

2.19 ms
2.09 ms

209 ms
214 ms

2.20 ms
2.04 ms

250 ms
250 ms

3.58 ms
3.73 ms

2.25 ms
2.10 ms

255 ms
258 ms

2.47 ms
2.58 ms

canada.json, js_traits

-

175 ms
177 ms

-

209 ms
213 ms

-

-

213 ms
214 ms

-

citm_catalog.json

1.28 ms
1.22 ms

27.7 ms
29.1 ms

1.35 ms
1.15 ms

36.3 ms
36.4 ms

1.32 ms
1.39 ms

1.39 ms
1.18 ms

37.3 ms
37.9 ms

1.04 ms
1.13 ms

citm_catalog.json, js_traits

-

28.5 ms
28.9 ms

-

36.6 ms
36.1 ms

-

-

36.7 ms
36.9 ms

-

twitter.json

751 µs
767 µs

11.6 ms
11.5 ms

830 µs
791 µs

15.1 ms
15.6 ms

589 µs
672 µs

835 µs
820 µs

16.0 ms
16.1 ms

403 µs
485 µs

twitter.json, js_traits

-

12.0 ms
12.0 ms

-

15.0 ms
15.7 ms

-

-

15.5 ms
15.7 ms

-

2. Stringify, compact

From nativejson-benchmark, Boost.JSON bench, nlohmann, simdjson, JetStream 2.1; time per document.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

canada.json

1.70 ms
1.95 ms

33.9 ms
34.8 ms

4.63 ms
6.16 ms

36.7 ms
38.9 ms

2.62 ms
2.31 ms

4.68 ms
6.05 ms

39.3 ms
41.8 ms

1.91 ms
2.39 ms

canada.json, js_traits

-

20.5 ms
21.8 ms

-

29.2 ms
33.4 ms

-

-

29.3 ms
32.2 ms

-

citm_catalog.json

348 µs
378 µs

4.23 ms
4.58 ms

498 µs
843 µs

4.85 ms
5.24 ms

745 µs
821 µs

510 µs
835 µs

4.87 ms
5.36 ms

582 µs
750 µs

citm_catalog.json, js_traits

-

4.45 ms
4.71 ms

-

5.36 ms
5.84 ms

-

-

5.30 ms
5.76 ms

-

twitter.json

281 µs
310 µs

1.35 ms
1.61 ms

419 µs
454 µs

1.66 ms
1.86 ms

255 µs
304 µs

426 µs
465 µs

1.69 ms
1.91 ms

182 µs
385 µs

twitter.json, js_traits

-

1.63 ms
1.84 ms

-

1.88 ms
2.13 ms

-

-

1.91 ms
2.17 ms

-

3. Prettify, indent 4

From nativejson-benchmark, nlohmann, simdjson; time per document.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

canada.json

3.58 ms *
4.43 ms

34.9 ms
35.0 ms

6.96 ms *
8.19 ms

37.7 ms
37.8 ms

6.34 ms
8.16 ms

7.00 ms *
8.15 ms

40.7 ms
41.0 ms

7.18 ms
7.68 ms

citm_catalog.json

900 µs *
921 µs

4.51 ms
4.63 ms

1.45 ms *
1.47 ms

5.20 ms
5.31 ms

2.01 ms
1.89 ms

1.50 ms *
1.52 ms

5.24 ms
5.30 ms

1.79 ms
1.87 ms

twitter.json

535 µs *
567 µs

1.44 ms
1.52 ms

792 µs *
831 µs

1.82 ms
1.84 ms

923 µs
928 µs

799 µs *
840 µs

1.81 ms
1.94 ms

626 µs
795 µs

4. Full traversal (Statistics)

From nativejson-benchmark; time per document.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

canada.json

186 µs
200 µs

37.0 ms
37.3 ms

192 µs
213 µs

36.9 ms
36.8 ms

1.20 ms
4.46 ms

190 µs
215 µs

44.6 ms
44.6 ms

1.12 ms
2.09 ms

citm_catalog.json

54.4 µs
60.0 µs

9.65 ms
9.80 ms

68.3 µs
73.9 µs

11.0 ms
11.0 ms

384 µs
433 µs

67.3 µs
75.0 µs

11.0 ms
11.3 ms

334 µs
415 µs

twitter.json

32.0 µs
29.8 µs

2.71 ms
2.72 ms

32.1 µs
38.4 µs

2.98 ms
3.05 ms

174 µs
229 µs

30.8 µs
40.0 µs

3.04 ms
3.12 ms

170 µs
215 µs

5. Key-path queries

From simdjson (find_tweet, top_tweet, distinct_user_id); time per query, parse excluded.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

find_tweet, twitter.json

108 ns
1.54 µs

1.51 µs
10.0 µs

162 ns
74.0 µs

1.90 µs
89.6 µs

41.7 ns
67.1 µs

137 ns
60.0 µs

1.94 µs
90.0 µs

53.6 ns
50.0 µs

top_tweet, twitter.json

930 ns
9.92 µs

17.0 µs
123 µs

1.75 µs
80.8 µs

20.3 µs
278 µs

274 ns
107 µs

1.75 µs
80.0 µs

20.3 µs
280 µs

284 ns
80.0 µs

distinct_user_id, twitter.json

4.94 µs
31.7 µs

59.5 µs
191 µs

7.29 µs
259 µs

73.5 µs
540 µs

8.91 µs
208 µs

7.50 µs
210 µs

74.6 µs
510 µs

8.06 µs
190 µs

6. Lookup, hit and miss

From Tessil, martinus, V8 PropertyQueries, abseil; time per lookup.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

10 members, 15-character keys, hit

10.4 ns
129 ns

42.3 ns
292 ns

8.86 ns
3.67 µs

56.0 ns
10.8 µs

5.65 ns
9.23 µs

9.24 ns
1.50 µs

56.3 ns
8.00 µs

5.25 ns
8.00 µs

10 members, 15-character keys, miss

15.6 ns
70.8 ns

23.7 ns
167 ns

15.5 ns
4.89 µs

33.6 ns
5.54 µs

8.53 ns
6.69 µs

15.7 ns
2.50 µs

33.1 ns
5.00 µs

7.87 ns
6.00 µs

10 members, 50-character keys, hit

9.69 ns
108 ns

81.7 ns
321 ns

11.1 ns
150 ns

93.4 ns
350 ns

5.35 ns
354 ns

10.9 ns
0.00 ns

93.6 ns
500 ns

5.15 ns
500 ns

10 members, 50-character keys, miss

14.3 ns
104 ns

22.2 ns
175 ns

12.8 ns
175 ns

32.4 ns
300 ns

8.73 ns
1.36 µs

13.2 ns
0.00 ns

32.2 ns
0.00 ns

7.79 ns
2.00 µs

1,000 members, 15-character keys, hit

13.0 ns
22.6 ns

132 ns
246 ns

16.7 ns
32.4 ns

189 ns
314 ns

5.57 ns
15.5 ns

16.2 ns
30.0 ns

179 ns
310 ns

4.85 ns
15.0 ns

1,000 members, 15-character keys, miss

7.97 ns
17.5 ns

80.1 ns
115 ns

9.06 ns
23.6 ns

127 ns
160 ns

11.5 ns
155 ns

7.80 ns
20.0 ns

128 ns
160 ns

7.71 ns
130 ns

1,000 members, 50-character keys, hit

17.2 ns
29.1 ns

197 ns
300 ns

26.0 ns
44.5 ns

254 ns
384 ns

5.69 ns
17.7 ns

25.7 ns
45.0 ns

248 ns
375 ns

4.91 ns
15.0 ns

1,000 members, 50-character keys, miss

13.3 ns
20.6 ns

83.8 ns
122 ns

11.4 ns
33.7 ns

133 ns
170 ns

11.2 ns
305 ns

11.5 ns
35.0 ns

133 ns
175 ns

8.29 ns
245 ns

100,000 members, 15-character keys, hit

19.0 ns
27.7 ns

576 ns
651 ns

27.2 ns
39.9 ns

705 ns
771 ns

24.2 ns
71.3 ns

27.9 ns
40.5 ns

707 ns
774 ns

15.9 ns
36.2 ns

100,000 members, 15-character keys, miss

17.5 ns
17.9 ns

279 ns
298 ns

22.5 ns
23.5 ns

378 ns
392 ns

19.8 ns
185 ns

22.6 ns
24.1 ns

384 ns
417 ns

15.5 ns
124 ns

100,000 members, 50-character keys, hit

28.0 ns
63.1 ns

932 ns
1.01 µs

38.5 ns
70.9 ns

965 ns
1.02 µs

16.7 ns
35.7 ns

39.0 ns
63.0 ns

1.06 µs
1.13 µs

13.9 ns
28.4 ns

100,000 members, 50-character keys, miss

20.2 ns
21.1 ns

351 ns
357 ns

28.5 ns
31.2 ns

434 ns
452 ns

18.3 ns
415 ns

29.5 ns
30.4 ns

555 ns
463 ns

14.9 ns
344 ns

citm_catalog.json events (184 keys), hit

8.81 ns
48.7 ns

124 ns
262 ns

12.4 ns
85.8 ns

153 ns
535 ns

41.1 ns
131 ns

11.7 ns
81.5 ns

153 ns
489 ns

33.4 ns
109 ns

citm_catalog.json events (184 keys), miss

4.79 ns
22.6 ns

51.9 ns
80.2 ns

7.51 ns
34.4 ns

65.5 ns
95.1 ns

43.3 ns
102 ns

7.36 ns
27.2 ns

62.5 ns
81.5 ns

29.7 ns
81.5 ns

7. Insert a member

From Tessil, abseil TryEmplace, V8 Map-Set; time per insertion.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

10 members, 15-character keys

30.2 ns
308 ns

435 ns
842 ns

27.8 ns
2.22 µs

617 ns
5.65 µs

6.26 ns
2.26 µs

28.1 ns
2.00 µs

647 ns
5.00 µs

6.09 ns
2.00 µs

10 members, 50-character keys

30.6 ns
338 ns

453 ns
992 ns

28.7 ns
396 ns

631 ns
1.26 µs

6.25 ns
208 ns

28.6 ns
1.00 µs

670 ns
1.00 µs

6.27 ns
0.00 ns

1,000 members, 15-character keys

32.6 ns
48.9 ns

652 ns
754 ns

40.2 ns
131 ns

959 ns
1.04 µs

19.8 ns
50.3 ns

41.6 ns
130 ns

992 ns
1.06 µs

15.4 ns
40.0 ns

1,000 members, 50-character keys

38.3 ns
53.7 ns

689 ns
770 ns

55.1 ns
136 ns

1.01 µs
1.06 µs

20.4 ns
48.0 ns

56.6 ns
130 ns

1.01 µs
1.05 µs

15.4 ns
40.0 ns

100,000 members, 15-character keys

38.6 ns
47.8 ns

1.14 µs
1.20 µs

50.8 ns
55.6 ns

1.59 µs
1.61 µs

43.3 ns
57.9 ns

51.6 ns
60.4 ns

1.63 µs
1.64 µs

34.4 ns
43.4 ns

100,000 members, 50-character keys

45.9 ns
63.4 ns

1.42 µs
1.43 µs

68.7 ns
74.8 ns

1.88 µs
1.91 µs

42.7 ns
52.0 ns

69.2 ns
75.5 ns

1.98 µs
2.03 µs

33.7 ns
35.0 ns

8. Update a member

From abseil InsertOrAssign, martinus; time per assignment.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

10 members, 15-character keys

12.3 ns
37.5 ns

143 ns
333 ns

12.9 ns
2.27 µs

205 ns
5.37 µs

5.12 ns
2.66 µs

12.8 ns
1.50 µs

203 ns
5.00 µs

4.72 ns
2.00 µs

10 members, 50-character keys

11.1 ns
66.6 ns

185 ns
367 ns

15.1 ns
75.0 ns

246 ns
488 ns

4.97 ns
738 ns

14.9 ns
0.00 ns

241 ns
1.00 µs

4.67 ns
500 ns

1,000 members, 15-character keys

18.1 ns
20.5 ns

258 ns
329 ns

26.6 ns
29.8 ns

397 ns
440 ns

4.95 ns
9.04 ns

27.1 ns
25.0 ns

391 ns
465 ns

4.04 ns
10.00 ns

1,000 members, 50-character keys

23.1 ns
25.6 ns

335 ns
385 ns

35.2 ns
37.4 ns

443 ns
503 ns

5.02 ns
9.71 ns

35.0 ns
40.0 ns

448 ns
540 ns

4.52 ns
10.00 ns

100,000 members, 15-character keys

21.7 ns
24.0 ns

633 ns
664 ns

31.1 ns
35.8 ns

797 ns
847 ns

8.66 ns
16.6 ns

31.2 ns
34.2 ns

805 ns
862 ns

7.32 ns
10.2 ns

100,000 members, 50-character keys

29.8 ns
42.9 ns

959 ns
1.03 µs

40.4 ns
46.2 ns

1.12 µs
1.18 µs

9.63 ns
11.3 ns

41.1 ns
49.6 ns

1.16 µs
1.21 µs

7.46 ns
8.05 ns

citm_catalog.json events (184 keys)

111 ns
125 ns

3.71 µs
4.57 µs

150 ns
123 ns

5.18 µs
6.49 µs

44.9 ns
63.4 ns

158 ns
136 ns

5.14 µs
6.52 µs

32.6 ns
54.3 ns

9. Delete a member, then lookups

From Tessil, martinus, V8 Map-Delete, Dromaeo removeChild; time per deletion, and per lookup after them.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

10 members, 15-character keys, 5 deletions

17.6 ns *
133 ns

177 ns
1.12 µs

19.8 ns *
8.92 µs

249 ns
14.2 µs

36.5 ns
14.3 µs

19.8 ns *
7.00 µs

251 ns
9.00 µs

21.2 ns
10.00 µs

10 members, 15-character keys, lookups after them

8.56 ns *
104 ns

37.7 ns
204 ns

7.16 ns *
150 ns

53.6 ns
392 ns

5.98 ns
417 ns

7.35 ns *
0.00 ns

52.9 ns
500 ns

5.15 ns
500 ns

10 members, 50-character keys, 5 deletions

21.9 ns *
167 ns

210 ns
1.09 µs

23.1 ns *
300 ns

286 ns
1.40 µs

36.0 ns
958 ns

23.3 ns *
1.00 µs

289 ns
2.00 µs

20.5 ns
1000 ns

10 members, 50-character keys, lookups after them

7.76 ns *
129 ns

57.3 ns
283 ns

8.26 ns *
408 ns

71.2 ns
404 ns

5.84 ns
417 ns

8.16 ns *
0.00 ns

70.2 ns
0.00 ns

5.27 ns
0.00 ns

1,000 members, 15-character keys, 500 deletions

2.44 µs *
2.36 µs

343 ns
514 ns

3.00 µs *
3.06 µs

511 ns
676 ns

12.9 ns
37.3 ns

2.66 µs *
3.33 µs

510 ns
690 ns

7.38 ns
20.0 ns

1,000 members, 15-character keys, lookups after them

9.52 ns *
18.0 ns

119 ns
208 ns

12.5 ns *
27.8 ns

193 ns
290 ns

6.50 ns
19.3 ns

11.9 ns *
25.0 ns

190 ns
290 ns

5.97 ns
15.0 ns

1,000 members, 50-character keys, 500 deletions

3.17 µs *
3.24 µs

401 ns
548 ns

4.46 µs *
4.41 µs

562 ns
733 ns

12.7 ns
37.6 ns

4.49 µs *
4.65 µs

568 ns
720 ns

7.74 ns
30.0 ns

1,000 members, 50-character keys, lookups after them

12.3 ns *
26.0 ns

158 ns
234 ns

18.4 ns *
32.7 ns

220 ns
314 ns

6.94 ns
22.6 ns

17.5 ns *
40.0 ns

217 ns
310 ns

5.81 ns
20.0 ns

100,000 members, 15-character keys, 1,000 deletions

644 µs *
640 µs

709 ns
1.12 µs

686 µs *
687 µs

955 ns
1.40 µs

16.0 ns
47.2 ns

679 µs *
685 µs

946 ns
1.46 µs

12.6 ns
35.0 ns

100,000 members, 15-character keys, lookups after them

22.1 ns *
28.2 ns

559 ns
622 ns

30.9 ns *
43.4 ns

709 ns
768 ns

15.4 ns
26.5 ns

30.6 ns *
41.8 ns

719 ns
793 ns

12.9 ns
27.9 ns

100,000 members, 50-character keys, 1,000 deletions

774 µs *
784 µs

985 ns
1.35 µs

949 µs *
945 µs

1.28 µs
1.62 µs

17.7 ns
34.0 ns

960 µs *
957 µs

1.33 µs
1.68 µs

12.9 ns
30.0 ns

100,000 members, 50-character keys, lookups after them

28.0 ns *
55.0 ns

894 ns
985 ns

42.2 ns *
70.8 ns

1.05 µs
1.17 µs

14.8 ns
21.2 ns

42.9 ns *
65.4 ns

1.05 µs
1.13 µs

12.2 ns
16.5 ns

citm_catalog.json events (184 keys), 92 deletions

552 ns *
591 ns

2.96 µs
4.52 µs

673 ns *
741 ns

3.87 µs
5.83 µs

60.9 ns
125 ns

674 ns *
707 ns

4.06 µs
6.03 µs

38.4 ns
54.3 ns

citm_catalog.json events (184 keys), lookups after them

6.12 ns *
31.9 ns

140 ns
252 ns

8.48 ns *
53.4 ns

179 ns
326 ns

48.8 ns
109 ns

8.35 ns *
54.3 ns

177 ns
326 ns

39.4 ns
109 ns

10. Iterate entries

From Tessil, martinus, abseil, V8 Keys and Object, Dromaeo traverse; time per member.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

10 members, 15-character keys

1.02 ns
113 ns

16.9 ns
204 ns

0.38 ns
4.49 µs

21.7 ns
9.27 µs

1.52 ns
8.24 µs

0.37 ns
4.00 µs

21.5 ns
6.50 µs

1.54 ns
6.50 µs

10 members, 50-character keys

1.02 ns
66.7 ns

16.8 ns
138 ns

0.36 ns
100 ns

21.2 ns
288 ns

1.52 ns
358 ns

0.35 ns
0.00 ns

21.0 ns
0.00 ns

1.51 ns
0.00 ns

1,000 members, 15-character keys

0.38 ns
1.38 ns

41.1 ns
127 ns

0.46 ns
1.79 ns

47.4 ns
135 ns

20.5 ns
116 ns

0.47 ns
5.00 ns

47.9 ns
130 ns

18.9 ns
100.0 ns

1,000 members, 50-character keys

0.38 ns
1.21 ns

43.0 ns
120 ns

0.46 ns
2.42 ns

52.6 ns
134 ns

20.9 ns
66.1 ns

0.47 ns
0.00 ns

54.7 ns
130 ns

18.5 ns
65.0 ns

100,000 members, 15-character keys

0.39 ns
0.49 ns

165 ns
171 ns

0.48 ns
0.53 ns

182 ns
184 ns

81.9 ns
88.2 ns

0.48 ns
0.55 ns

173 ns
177 ns

79.2 ns
82.7 ns

100,000 members, 50-character keys

0.38 ns
0.52 ns

258 ns
271 ns

0.48 ns
0.54 ns

278 ns
281 ns

80.7 ns
83.3 ns

0.48 ns
0.55 ns

213 ns
212 ns

79.2 ns
80.0 ns

citm_catalog.json events (184 keys)

0.37 ns
4.30 ns

43.7 ns
153 ns

0.42 ns
6.79 ns

47.4 ns
163 ns

96.7 ns
235 ns

0.43 ns
0.00 ns

50.6 ns
163 ns

75.1 ns
190 ns

11. Array push and pop

From Dromaeo appendChild and removeChild, martinus; time per push or pop.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

twitter.json statuses (100)

3.05 ns
19.0 ns

789 ns
987 ns

3.87 ns
538 ns

1.02 µs
2.15 µs

0.79 ns
474 ns

3.94 ns
475 ns

1.04 µs
2.08 µs

1.07 ns
425 ns

canada.json’s largest ring (14,310 points)

3.11 ns
47.3 ns

1.26 µs
1.46 µs

3.83 ns
34.0 ns

1.57 µs
1.76 µs

0.82 ns
98.1 ns

3.96 ns
25.0 ns

1.61 µs
1.88 µs

1.08 ns
25.0 ns

12. Array splice in the middle

From Dromaeo insertBefore and removeChild; time per splice.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

canada.json’s largest ring (14,310 points)

1.82 µs *
3.27 µs

666 µs
836 µs

855 ns *
41.2 µs

832 µs
978 µs

2.66 µs
54.5 µs

865 ns *
37.5 µs

853 µs
1.07 ms

2.80 µs
37.5 µs

13. Array methods

From V8 js-perf-test Array; time per element.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

map, canada.json’s largest ring (14,310 points)

5.31 ns *
5.13 ns

1.00 µs *
1.06 µs

6.96 ns *
24.4 ns

1.17 µs *
1.27 µs

6.20 ns
18.2 ns

6.98 ns *
21.0 ns

1.25 µs *
1.38 µs

5.37 ns
15.7 ns

filter, canada.json’s largest ring (14,310 points)

8.37 ns *
35.1 ns

807 ns *
843 ns

9.21 ns *
40.1 ns

920 ns *
1.01 µs

5.64 ns
18.6 ns

9.40 ns *
42.3 ns

1.00 µs *
1.11 µs

4.37 ns
16.1 ns

reduce, twitter.json statuses (100)

7.80 ns *
109 ns

162 ns *
1.69 µs

14.7 ns *
583 ns

191 ns *
2.63 µs

1.72 ns
755 ns

14.4 ns *
550 ns

195 ns *
2.75 µs

1.73 ns
650 ns

find, twitter.json statuses (100)

18.1 ns *
186 ns

243 ns *
1.18 µs

23.4 ns *
603 ns

293 ns *
3.03 µs

2.85 ns
1.05 µs

22.9 ns *
750 ns

296 ns *
2.45 µs

2.57 ns
800 ns

slice, canada.json’s largest ring (14,310 points)

18.4 ns *
41.0 ns

1.41 µs
1.44 µs

22.5 ns *
53.7 ns

1.75 µs
1.88 µs

0.09 ns
7.33 ns

22.6 ns *
87.4 ns

1.81 µs
1.98 µs

0.05 ns
4.89 ns

join, twitter.json statuses (100)

15.2 ns *
232 ns

148 ns *
1.50 µs

19.9 ns *
809 ns

178 ns *
3.48 µs

10.3 ns *
860 ns

18.9 ns *
850 ns

179 ns *
2.90 µs

9.28 ns *
700 ns

14. Deep copy

From martinus Copy, abseil CopyCtor, Dromaeo cloneNode; time per document.

Dataset Boost.JSON native document native Boost.JSON wasm, Node document wasm, Node JavaScript, Node Boost.JSON wasm, Chrome document wasm, Chrome JavaScript, Chrome

canada.json, structuredClone in JavaScript

1.12 ms
924 µs

441 µs
474 µs

1.20 ms
1.00 ms

438 µs
546 µs

5.43 ms
5.69 ms

1.24 ms
1.03 ms

440 µs
555 µs

4.38 ms
5.04 ms

citm_catalog.json, structuredClone in JavaScript

742 µs
546 µs

211 µs
239 µs

755 µs
456 µs

210 µs
235 µs

3.21 ms
3.50 ms

760 µs
475 µs

211 µs
245 µs

1.96 ms
2.25 ms

twitter.json, structuredClone in JavaScript

373 µs
335 µs

49.2 µs
61.7 µs

393 µs
281 µs

48.6 µs
61.5 µs

813 µs
994 µs

401 µs
305 µs

49.4 µs
60.0 µs

517 µs
760 µs

canada.json, JSON.parse(JSON.stringify(v))

-

-

-

-

6.02 ms
5.78 ms

-

-

4.26 ms
4.77 ms

citm_catalog.json, JSON.parse(JSON.stringify(v))

-

-

-

-

1.70 ms
1.78 ms

-

-

1.18 ms
1.33 ms

twitter.json, JSON.parse(JSON.stringify(v))

-

-

-

-

759 µs
865 µs

-

-

517 µs
745 µs

Reading the results

The figures below are this machine’s medians natively unless a place is named; the tables hold the rest. document is not a fast JSON tree, and this first version does not try to be one: it pays, on every operation, for what it guarantees, JavaScript’s behaviour, exact numbers and one relocatable image. Where it is slower, and why:

  • Binary transmission, which the image is built for. A document sent as its image is copied and opened in place: the receiver parses no text, builds no tree and allocates nothing per value, so its CPU cost is the copy and, for a sealed image, one CRC pass over the bytes (about 600 MB/s). Boost.JSON and JavaScript pay the parse on the receiving side, which the image does not. That is where the image wins: on the receiver’s CPU, in a Worker, where the image crosses as a transferred ArrayBuffer (row 17) and not as a structured clone of every node, and in shared memory, which is not measured here. The rows’ timings are in the tables above. Over a constrained network the text wins: the image is 8 to 17 times larger, and still larger compressed, so on a link that carries the bytes the text is faster unless the receiver’s parse costs more than the extra bytes take to cross. On a LAN it depends on the rate. For canada.json the image is 30 MB more than the text, which the link saves only when it carries more than about 143 MB/s (1.1 Gbit/s) against document’s own parse of canada, 209 ms; Boost.JSON’s 2.19 ms parse is never worth it. The seal is a CRC against accidental damage, not authentication: anyone can compute a valid seal, so an image from an untrusted source must not be opened.

  • Building and walking the tree. document parses canada.json in 209 ms, where Boost.JSON takes 2.19 ms and Node’s JSON.parse 3.58 ms: 95 times Boost.JSON’s time, 22 times on citm_catalog.json and 15 times on twitter.json. A full traversal is slower still, 37.0 ms against 186 us on canada.json. Every value is a node of a Boost.MultiIndex container inside the image, linked into three red-black trees by offset pointers: by its id, by its parent and position, and by its parent and key. Appending a value finds its parent by id and links the node into each tree, and a handle names a node by its id, so that every access looks it up again. A profile of the native parse of canada.json (macOS’s sample) spends most of its time looking nodes up by id, inserting into and rebalancing the trees, and converting numbers to exact decimals; under js_traits, whose numbers are doubles, the same parse takes 175 ms, so the tree, not the decimals, is the larger cost. Boost.JSON appends to contiguous arrays and keeps doubles.

  • Stringify is 20 times Boost.JSON’s time on canada.json (33.9 ms against 1.70 ms) and 5 times on twitter.json: it walks the same trees, and writes each exact decimal as JavaScript would write the number; js_traits brings canada.json down to 20.5 ms.

  • Dictionaries. A lookup finds its key in an ordered tree, by comparisons, where Boost.JSON and V8 hash it: 42 ns at 10 members, 576 ns at 100,000, against 10 to 28 ns for Boost.JSON and 5 to 24 ns for JavaScript. Insertion (0.4 to 1.4 us), assignment and iteration (17 to 258 ns a member, against under 1 ns for Boost.JSON’s contiguous members) pay the same trees and the same lookups by id. JavaScript is slower than Boost.JSON on the events of citm_catalog.json (41 ns a lookup): its keys are array indices, which V8 keeps apart from named properties.

  • Arrays. A splice in the middle of 14,310 points takes 666 us, against 2 or 3 us for Boost.JSON and JavaScript: every element after the splice is renumbered in the position tree. A push and a pop take 789 ns each, because pop returns the removed value as a document of its own, whose image it allocates; set_length would drop it without one. map and filter copy each element they keep into a new document, about 1 us an element, where JavaScript’s share them, as its slice does.

  • Where document is faster: a deep copy is one copy of the image, 441 us for canada.json, against 1.12 ms for Boost.JSON’s copy constructor and 5.43 ms for structuredClone in Node. And deleting from a large object keeps the order without moving the members: 709 ns a deletion from 100,000 members, where Boost.JSON’s stable_erase takes 644 us.

Across places: the C++ compiled to WebAssembly takes a median 1.26 times its native time, for Boost.JSON and for document alike, at most 2.7 times (Boost.JSON’s stringify of canada.json, whose doubles it formats more slowly in wasm32), and Node and Chrome run the same module within a quarter of each other, their V8 sharing a major version. Boost.JSON’s splice is even faster in WebAssembly (855 ns) than natively (1.82 us): its values are 16 bytes in wasm32, 24 natively, and a splice moves them. JavaScript’s first iteration is often several times its median, the time V8 takes to compile it, where the C++'s first pass runs close to its median. Chrome’s clock reads 5 us, so the first iteration of a pass shorter than that, such as ten lookups, reads in Chrome as 0 or 5 us divided by its operations.

These costs are the design’s, measured before any tuning, and the profile names where to begin: the lookup of a node by id behind every handle, the renumbering of positions on a splice, and the image that pop allocates.

Running them

b2 libs/document/bench//results
b2 libs/document/bench//smoke toolset=clang-wasip2 testing.launcher=wasmtime

The first measures on the machine it runs on, which took 9 minutes here, and writes doc/benchmark-results.adoc; it is explicit, so that neither a default build nor the continuous integration runs it. Node, npm and Emscripten’s toolset are needed beside the native one: npm installs Playwright, and Playwright its headless shell, once. The second is one verifying pass of every operation, on any target, which fails when Boost.JSON and document answer differently; the library’s tests run it on every target as bench, without the objects of 100,000 members, which run the same code as those of 1,000 and would take most of its time in a debug build.

Reference

Every name document declares, in the namespace webcpp::document, from the Doc Comments of its headers: each class, function, type and enumerator with its declaration, what it does, what it asks of its caller and how it fails. The other sections teach the library; this one is where a name is looked up.

Macros

Name Description

WEBCPP_DOCUMENT_NO_DANGLING

Marks a class that names a place in a document and owns what it needs to find it again, a handle or a range of a container’s children; empty outside GCC 14 and later, whose -Wdangling-reference it quiets.

WEBCPP_DOCUMENT_NO_EXCEPTIONS

Defined when document is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone.

webcpp::document

Types

Name Description

array

An array, as JavaScript writes [...]: wd::array{1, "x", nullptr}.

base64

The bytes policy that writes bytes as base64, RFC 4648 with padding, the default.

base64url

The bytes policy that writes bytes as base64url, RFC 4648 section 5, without padding.

basic_array_ref

An array as a vector-like container: front, back, operator[], at, push_back, emplace_back, pop_back, insert, erase, resize, clear and iteration both ways.

basic_const_value

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

basic_document

A JSON document: one value, in one relocatable image.

basic_image_reader

Reads a stream of images, one after another with nothing between them, into the blocks of an image_pool, and opens each in place as a leased_document.

basic_object_by_key

The members of an object in lexicographic key order, by UTF-16 code units whatever the encoding, with find, lower_bound, upper_bound and equal_range: prefix and range queries over keys.

basic_object_ref

An object as a sorted-map-like container, in the traits’ key order (JavaScript’s by default): find, insert, insert_or_assign, try_emplace, erase, iteration both ways, and by_key() for prefix and range queries in lexicographic key order.

basic_parser

The streaming parser: write takes the text in chunks, finish returns the document.

basic_stream_parser

The parser of a stream of documents: write takes the bytes in chunks of any size, down to one byte, and each document is delivered as soon as the bytes that end it are consumed.

basic_value

A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.

capacity_error

No room left: a full external buffer, a document at its limit, a parser’s limit.

decimal_digits

A finite decimal number as JavaScript’s Number::toString sees it: its value is 0.d1 d2 ... dk times ten to the power point, with k the count of its significant digits.

default_traits

The default traits: exact numbers, UTF-8 strings, base64 bytes and JavaScript’s key order.

dense

The array policy: no holes, and writing past the end fills with null.

error

What every exception of document derives from: a std::runtime_error holding its code.

external

The storage of a document whose image is a buffer its user owns, such as a browser’s memory: when it is full, an operation returns errc::capacity and changes nothing.

image_error

Bytes that are not a document’s image, an image of another format, or a damaged one.

image_header

The 64 bytes at the start of every image, in the byte order of the machine that wrote it.

image_pool

Fixed blocks for images, 16-aligned and zeroed, allocated once: block_bytes times blocks.

insertion_order

The object policy that keeps every key in the order it was created, whatever the key.

javascript_bytes

The bytes policy that writes bytes as JSON.stringify writes a Uint8Array: {"0":1,"1":2}.

javascript_order

The object policy of JavaScript: array-index keys ascending, then the others as created.

js_traits

The traits of JavaScript itself: doubles, UTF-16 strings, and bytes written as a Uint8Array.

leased_document

A document opened in place in a block of an image_pool, holding the block’s lease: when the document goes, the block goes back to the pool, its bytes zeroed.

literal

One value of a literal, converted implicitly from the C++ value it is written as.

null_t

The type of null, JavaScript’s null.

number_codec

How the traits’ number type is read from JSON text, written as JavaScript writes it, and converted; the primary template has none.

number_text

Text of at most 64 characters, held by value, so that writing a number or a date allocates nothing.

object

An object, as JavaScript writes {...}: wd::object{{"city", "Recife"}, {"zip", "50000"}}.

object_member

One member of an object literal: its key, in UTF-8, and its value.

owned

The storage of a document that owns its image: a buffer of its own, 16-aligned, which grows, twice the size each time, up to its max_bytes.

parse_options

The limits and the revivers of a parse.

path_view

The path in the text of a value a selector delivers: each step a member’s key or an element’s index in the text, which on does not renumber.

pool_block

A lease of one block of an image_pool; the block is its holder’s until the lease is destroyed or assigned, and then goes back to the pool, its bytes zeroed.

range_error

JavaScript’s RangeError: a number that does not fit, or an index out of range.

scalar_codec

How a scalar type is stored in a node: its name in the image’s layout tag, its size, and its bytes, written the same on every target.

shared_document

A document in a block of a shared_pool, holding one reference to it, as std::shared_ptr holds its object: copying adds a reference, destroying drops one, and the last reference zeroes the block and gives it back to the pool.

shared_pool

A pool of blocks for shared documents, over a region of memory the program gives: the process heap, or a region of shared memory that several processes map, each at an address of its own.

sorted

The object policy that orders keys by their UTF-16 code units, with either encoding, as JavaScript’s sort() and RFC 8785 do, for canonical JSON.

stream_options

The options of a stream: the limits and the revivers of each of its documents, its framing and its policy on errors.

syntax_error

JavaScript’s SyntaxError: malformed JSON text, at a byte offset.

type_error

JavaScript’s TypeError: a value of another kind, or a write through nothing.

undefined_t

The type of undefined, JavaScript’s undefined, which set takes to erase a member.

utf16

The encoding of strings and keys that stores UTF-16 code units, as JavaScript holds strings.

utf8

The encoding of strings and keys that stores UTF-8, the default.

Type Aliases

Name Description

array_ref

A view of an array of a document, reading and writing.

const_array_ref

A view of an array of a document, read only.

const_object_ref

A view of an object of a document, read only.

const_value

A handle of a document, read only.

date_ms

A date as JavaScript’s Date holds one: milliseconds since the epoch, 1970-01-01T00:00:00Z.

document

A document of the default traits, owned: exact numbers, UTF-8, JavaScript’s key order.

image_reader

A reader of images of the default traits.

object_ref

A view of an object of a document, reading and writing.

parser

The streaming parser of a document.

result

What every operation of document that can fail returns: a T, or the error_code of the failure.

stream_parser

The parser of a stream of document values.

value

A handle of a document, reading and writing.

Enums

Name Description

errc

The errors document reports, as boost::system::error_code values of the category error_category.

framing

How the documents of a stream are told apart.

kind

The kind of a value: JavaScript’s typeof, with null, arrays, dates and bytes told apart.

on_error

What a stream does with a bad document.

verify

Whether opening an image checks its seal.

Functions

Name Description

async_read_document

async_read_document overloads

async_read_image

Reads the next image of a stream into a block of a pool and opens it there, with no copy and no allocation.

async_write_image

Writes a document’s image to a stream as one buffer, after sealing the document.

async_write_images

Writes the images of a range of documents to a stream in one gather write, after sealing every document of the range.

check_image

Checks that bytes are an image this build reads, reading nothing past the header before its size is known, and that a sealed image’s bytes match its seal.

error_category

The category of every errc.

format

Formats a buffer as a new, empty document, holding null.

from_decimal

The digits of a finite value of a decimal type, normalised: every member of a cohort, 1.5 and 1.50, has the same digits.

from_decimal128

The digits of a finite decimal128_t, normalised, as from_decimal gives them.

from_image

Copies an image into a new owned document.

from_int64

The digits of a signed 64-bit integer.

from_uint64

The digits of an unsigned 64-bit integer.

make_error_code

Makes the error_code of an errc, as its implicit conversion does.

open

Opens the image already in a buffer, in place.

parse

JSON.parse(text): reads JSON text into a new owned document.

parse_into

Reads JSON text into a new document in a buffer its user owns, as parse does.

parse_or_throw

JSON.parse(text), throwing on failure as JavaScript does; only with exceptions.

scan_number

Reads a JSON number’s text exactly, never through a double.

stringify

stringify overloads

stringify_to

stringify_to overloads

to_decimal

Converts a number to a decimal type, exactly.

to_decimal128

Converts a number to decimal128_t, exactly, as to_decimal does.

to_int64

Converts a number to a signed 64-bit integer, when it is one.

to_uint64

Converts a number to an unsigned 64-bit integer, when it is one.

traits_tag

The traits’ identity in the image’s layout tag: an FNV-1a hash, 32 bits, of each scalar codec’s name and size (boolean, integer, number, date), then the names of the string, bytes, object and array policies, each followed by a zero byte.

write_javascript

Writes a number as JavaScript’s Number::toString writes it, ECMA-262, radix 10.

operator==

Equality operators

Variables

Name Description

byte_order_mark

The byte-order mark of the header, as the machine that wrote it writes this number.

default_chunk

The size of the pieces stringify_to hands its sink, unless told otherwise.

default_max_bytes

The largest image an owned document grows to unless told otherwise: 1 GiB.

format_version

The version of the image’s format that this library writes and opens.

image_magic

The first eight bytes of every image: WCPPDOC and a zero byte.

null

JavaScript’s null, which set stores, as nullptr does.

undefined

JavaScript’s undefined: v.set(wd::undefined) erases a member, as delete does, and stores null in an element.

Concepts

Name Description

decimal_type

The decimal types of Boost.Decimal a document converts to and from: decimal32_t, decimal64_t, decimal128_t and their fast variants.

document_traits

What a document’s traits must give: the scalar types, each a stored_scalar, a number_codec for number, and one of the encodings and policies.

stored_scalar

A type a node can store as a scalar: trivially copyable, with a scalar_codec whose size is 1, 2, 4, 8 or 16 bytes.

webcpp::document::array_ref

A view of an array of a document, reading and writing.

Synopsis

using array_ref = basic_array_ref<value>;

webcpp::document::const_array_ref

A view of an array of a document, read only.

Synopsis

using const_array_ref = basic_array_ref<const_value>;

webcpp::document::const_object_ref

A view of an object of a document, read only.

Synopsis

using const_object_ref = basic_object_ref<const_value>;

webcpp::document::const_value

A handle of a document, read only.

Synopsis

using const_value = basic_const_value<>;

webcpp::document::date_ms

A date as JavaScript’s Date holds one: milliseconds since the epoch, 1970-01-01T00:00:00Z.

Synopsis

using date_ms = std::chrono::sys_time<std::chrono::milliseconds>;

webcpp::document::document

A document of the default traits, owned: exact numbers, UTF-8, JavaScript’s key order.

Synopsis

using document = basic_document<>;

webcpp::document::image_reader

A reader of images of the default traits.

Synopsis

using image_reader = basic_image_reader<>;

webcpp::document::object_ref

A view of an object of a document, reading and writing.

Synopsis

using object_ref = basic_object_ref<value>;

webcpp::document::parser

The streaming parser of a document.

Synopsis

using parser = basic_parser<>;

webcpp::document::result

What every operation of document that can fail returns: a T, or the error_code of the failure.

Synopsis

template<class T>
using result = boost::system::result<T>;

Description

It is Boost.System’s result. document’s core never throws: every operation of it that can fail returns one, with an errc, or the error a sink or a selector’s callback of the user’s returned, passed on unchanged.

Template Parameters

Name Description

T

The type of the value; void for an operation that returns none.

webcpp::document::stream_parser

The parser of a stream of document values.

Synopsis

using stream_parser = basic_stream_parser<>;

webcpp::document::value

A handle of a document, reading and writing.

Synopsis

using value = basic_value<>;

webcpp::document::array

An array, as JavaScript writes [...]: wd::array{1, "x", nullptr}.

Synopsis

class array;

Description

An array whose one element is an array writes that element as a literal, wd::array{wd::literal(wd::array{1})} for [[1]]: braces around one value of the class’s own type are a copy of that value where the compiler applies CWG 1467 to every class (MSVC, Clang before 20), so wd::array{wd::array{1}} is [1] there and [[1]] elsewhere.

Member Functions

Name Description

array [constructor]

Constructors

elements

The elements, in order.

See Also

"Handles", in the guide.

webcpp::document::array::array

Constructors

Synopses

An empty array, [].

constexpr
array() = default;

An array of these elements.

array(std::initializer_list<literal> elements);

Parameters

Name Description

elements

The elements, in order.

webcpp::document::array::array

An empty array, [].

Synopsis

constexpr
array() = default;

webcpp::document::array::array

An array of these elements.

Synopsis

array(std::initializer_list<literal> elements);

Parameters

Name Description

elements

The elements, in order.

webcpp::document::array::elements

The elements, in order.

Synopsis

[[nodiscard]]
std::span<literal const>
elements() const noexcept;

Return Value

A view of them.

Note

The return value should not be discarded.

webcpp::document::base64

The bytes policy that writes bytes as base64, RFC 4648 with padding, the default.

Synopsis

struct base64;

Static Data Members

Name Description

name

The policy’s name in the image’s layout tag.

See Also

"Strings, dates and bytes", in the guide.

webcpp::document::base64::name

The policy’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "base64";

webcpp::document::base64url

The bytes policy that writes bytes as base64url, RFC 4648 section 5, without padding.

Synopsis

struct base64url;

Static Data Members

Name Description

name

The policy’s name in the image’s layout tag.

webcpp::document::base64url::name

The policy’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "base64url";

webcpp::document::basic_array_ref

An array as a vector-like container: front, back, operator[], at, push_back, emplace_back, pop_back, insert, erase, resize, clear and iteration both ways.

Synopsis

template<class Handle>
class basic_array_ref;

Description

Made by as_array() on a handle, it holds the document’s address and the array’s id, and is cheap to copy: a reference, like std::span, whose constness is its own, so a const view still writes; const_array_ref is the view that only reads. It is a std::ranges::bidirectional_range, a view and a borrowed range. The tree finds an element by its index in O(log n), so operator[] costs O(log n), not O(1), and the iterators are bidirectional, not random access, whose O(1) jumps they could not keep.

Iterators and handles hold ids, which no change reuses:

| Change | Iterators and handles | A std::string_view read from a value | | --- | --- | --- | | An element inserted, and any growth of the image | all follow their elements | invalid | | An element erased | those to it and its subtree; the others follow their elements | invalid | | An element’s value replaced | those to its old subtree | invalid | | The document moved from or destroyed | all | invalid |

What can fail returns result: an insertion, which needs room in the image, and at. An erasure needs no room and cannot fail. There are no throwing forms: they would differ from these only in their return type, which C++ does not overload on.

Type Aliases

Name Description

difference_type

The difference of two positions.

iterator

A bidirectional iterator in index order.

reference

What dereferencing an iterator gives: a handle, made at each step.

reverse_iterator

A reverse iterator in index order.

size_type

A count of elements, or an index.

storage_type

The document’s storage.

traits_type

The document’s traits.

value_type

An element: its handle.

Member Functions

Name Description

basic_array_ref [constructor]

A view of nothing, which is empty and fails every insertion with errc::type.

at

The element index, checked, in O(log n).

back

The last element.

begin

The first element.

clear

Erases every element, which needs no room.

emplace_back

Appends v and gives its handle.

empty

Whether the array has no element.

end

The end of the elements, which every change leaves valid.

erase

erase overloads

front

The first element.

insert

Inserts v before pos, the later elements moved one index up, as a.splice(i, 0, v).

operator[]

The element index, a[index], in O(log n): the tree finds it by its index.

pop_back

Erases the last element, if there is one, which needs no room.

push_back

Appends v, a.push(v).

rbegin

The last element, first in reverse.

rend

The end of the elements in reverse.

resize

Truncates the array to n elements, or appends null up to n: a.length = n.

size

The array’s length.

Friends

Name Description

webcpp::document::basic_value

A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.

webcpp::document::basic_const_value

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

Template Parameters

Name Description

Handle

The handle its elements are read through: basic_value, to write, or basic_const_value.

See Also

"Handles", in the guide.

"Arrays", in the guide.

webcpp::document::basic_array_ref::difference_type

The difference of two positions.

Synopsis

using difference_type = std::ptrdiff_t;

webcpp::document::basic_array_ref::iterator

A bidirectional iterator in index order.

Synopsis

using iterator = /* implementation-defined */;

webcpp::document::basic_array_ref::reference

What dereferencing an iterator gives: a handle, made at each step.

Synopsis

using reference = Handle;

webcpp::document::basic_array_ref::reverse_iterator

A reverse iterator in index order.

Synopsis

using reverse_iterator = std::reverse_iterator<iterator>;

webcpp::document::basic_array_ref::size_type

A count of elements, or an index.

Synopsis

using size_type = uint64_t;

webcpp::document::basic_array_ref::storage_type

The document’s storage.

Synopsis

using storage_type = /* implementation-defined */::storage;

webcpp::document::basic_array_ref::traits_type

The document’s traits.

Synopsis

using traits_type = /* implementation-defined */::traits;

webcpp::document::basic_array_ref::value_type

An element: its handle.

Synopsis

using value_type = Handle;

webcpp::document::basic_array_ref::basic_array_ref

A view of nothing, which is empty and fails every insertion with errc::type.

Synopsis

basic_array_ref() noexcept = default;

webcpp::document::basic_array_ref::at

The element index, checked, in O(log n).

Synopsis

[[nodiscard]]
result<Handle>
at(size_type index) const noexcept;

Return Value

Its handle; errc::range past the end, errc::type once this is no array.

Note

The return value should not be discarded.

Parameters

Name Description

index

The index.

webcpp::document::basic_array_ref::back

The last element.

Synopsis

[[nodiscard]]
Handle
back() const noexcept;

Return Value

Its handle; one that names nothing when the array is empty.

Note

The return value should not be discarded.

webcpp::document::basic_array_ref::begin

The first element.

Synopsis

[[nodiscard]]
iterator
begin() const noexcept;

Return Value

The iterator; the end when there is none.

Note

The return value should not be discarded.

webcpp::document::basic_array_ref::clear

Erases every element, which needs no room.

Synopsis

void
clear() const noexcept
requires detail::writable_handle<Handle>;

webcpp::document::basic_array_ref::emplace_back

Appends v and gives its handle.

Synopsis

template<class T>
[[nodiscard]]
result<Handle>
emplace_back(T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;

Return Value

The new element’s handle; the errors of push_back.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The value’s type, any that set takes but undefined.

Parameters

Name Description

v

The value; a handle or a document is copied deeply, null for one that reads undefined.

webcpp::document::basic_array_ref::empty

Whether the array has no element.

Synopsis

[[nodiscard]]
bool
empty() const noexcept;

Return Value

Whether size is 0.

Note

The return value should not be discarded.

webcpp::document::basic_array_ref::end

The end of the elements, which every change leaves valid.

Synopsis

[[nodiscard]]
iterator
end() const noexcept;

Return Value

The iterator.

Note

The return value should not be discarded.

webcpp::document::basic_array_ref::erase

erase overloads

Synopses

Erases the element at pos and its subtree, the later elements moved one index down.

iterator
erase(iterator pos) const noexcept
requires detail::writable_handle<Handle>;

Erases the elements from first to last, last excluded, and their subtrees.

iterator
erase(
    iterator first,
    iterator last) const noexcept
requires detail::writable_handle<Handle>;

Return Value

  • The iterator of the element after it; the end when pos is the end, of another container, or at an element already erased.

  • last, which follows its element; the end when either is of another container or at an element already erased, or last is before first, which erases nothing.

Parameters

Name Description

pos

An iterator of this array at an element.

first

An iterator of this array.

last

An iterator of this array at or after first.

webcpp::document::basic_array_ref::erase

Erases the element at pos and its subtree, the later elements moved one index down.

Synopsis

iterator
erase(iterator pos) const noexcept
requires detail::writable_handle<Handle>;

Return Value

The iterator of the element after it; the end when pos is the end, of another container, or at an element already erased.

Parameters

Name Description

pos

An iterator of this array at an element.

webcpp::document::basic_array_ref::erase

Erases the elements from first to last, last excluded, and their subtrees.

Synopsis

iterator
erase(
    iterator first,
    iterator last) const noexcept
requires detail::writable_handle<Handle>;

Return Value

last, which follows its element; the end when either is of another container or at an element already erased, or last is before first, which erases nothing.

Parameters

Name Description

first

An iterator of this array.

last

An iterator of this array at or after first.

webcpp::document::basic_array_ref::front

The first element.

Synopsis

[[nodiscard]]
Handle
front() const noexcept;

Return Value

Its handle; one that names nothing when the array is empty.

Note

The return value should not be discarded.

webcpp::document::basic_array_ref::insert

Inserts v before pos, the later elements moved one index up, as a.splice(i, 0, v).

Synopsis

template<class T>
[[nodiscard]]
result<iterator>
insert(
    iterator pos,
    T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;

Return Value

The new element’s iterator; errc::type once this is no array or for an iterator of another container or at an element already erased, errc::capacity when there is no room, and the errors of set for the value.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The value’s type, any that set takes but undefined.

Parameters

Name Description

pos

An iterator of this array, the end to append.

v

The value; a handle or a document is copied deeply, null for one that reads undefined.

webcpp::document::basic_array_ref::operator[]

The element index, a[index], in O(log n): the tree finds it by its index.

Synopsis

Handle
operator[](size_type index) const noexcept;

Return Value

Its handle, as a handle’s operator[] gives it: past the end, a missing element, which set creates.

Parameters

Name Description

index

The index.

webcpp::document::basic_array_ref::pop_back

Erases the last element, if there is one, which needs no room.

Synopsis

void
pop_back() const noexcept
requires detail::writable_handle<Handle>;

webcpp::document::basic_array_ref::push_back

Appends v, a.push(v).

Synopsis

template<class T>
[[nodiscard]]
result<void>
push_back(T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;

Return Value

Success; errc::type once this is no array, errc::capacity when there is no room, and the errors of set for the value.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The value’s type, any that set takes but undefined.

Parameters

Name Description

v

The value; a handle or a document is copied deeply, null for one that reads undefined.

webcpp::document::basic_array_ref::rbegin

The last element, first in reverse.

Synopsis

[[nodiscard]]
reverse_iterator
rbegin() const noexcept;

Return Value

The reverse iterator.

Note

The return value should not be discarded.

webcpp::document::basic_array_ref::rend

The end of the elements in reverse.

Synopsis

[[nodiscard]]
reverse_iterator
rend() const noexcept;

Return Value

The reverse iterator.

Note

The return value should not be discarded.

webcpp::document::basic_array_ref::resize

Truncates the array to n elements, or appends null up to n: a.length = n.

Synopsis

[[nodiscard]]
result<void>
resize(size_type n) const noexcept
requires detail::writable_handle<Handle>;

Return Value

Success; errc::type once this is no array, errc::capacity when there is no room.

Note

The return value should not be discarded.

Parameters

Name Description

n

The new length.

webcpp::document::basic_array_ref::size

The array’s length.

Synopsis

[[nodiscard]]
size_type
size() const noexcept;

Return Value

The count; 0 once it is no array.

Note

The return value should not be discarded.

webcpp::document::basic_const_value

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

Synopsis

template<
    class Traits = default_traits,
    class Storage = owned>
class basic_const_value;

Description

A handle names a node, a missing member or element of a container that exists (it reads undefined, and basic_value::set creates it), or nothing (it reads undefined, and set fails with errc::type). It holds the document’s address and node ids, so it stays valid when the document grows and its image moves; it reads undefined once its node is erased, and is invalid once its document is moved from or destroyed.

Member Functions

Name Description

basic_const_value [constructor]

A handle that names nothing.

as

Reads the value as T, as get does, throwing on failure; only with exceptions.

as_array

The array as a vector-like container that reads: front, back, at, and iterators that <algorithm> and <ranges> take.

as_bool

Reads a boolean, throwing on failure; only with exceptions.

as_object

The object as a sorted-map-like container that reads: find, bounds by key, and iterators that <algorithm> and <ranges> take.

as_string

Reads a string as std::string_view, throwing on failure; only with exceptions.

at

Array.prototype.at: the element index, a negative index counting from the end.

begin

The first element of an array, for for (auto v : arr).

concat

Array.prototype.concat: this array, then each argument, an array’s elements or the value.

contains

Whether an object has the member key, key in v.

end

The end of an array’s elements.

entries

Object.entries: pairs of each key and its value, in the policy’s order.

get

Reads the value as T.

is_array

Whether the value is an array, Array.isArray.

is_boolean

Whether the value is true or false.

is_bytes

Whether the value is bytes.

is_date

Whether the value is a date.

is_integer

Whether the value is a number that is an integer, JavaScript’s Number.isInteger.

is_null

Whether the value is null.

is_number

Whether the value is a number.

is_object

Whether the value is a plain object.

is_string

Whether the value is a string.

is_undefined

Whether nothing is there, v === undefined.

keys

Object.keys: each key, in the policy’s order; a std::string_view with utf8, a std::u16string_view with utf16.

kind

The kind of the value, JavaScript’s typeof with null, arrays, dates and bytes told apart.

length

An array’s length, or a string’s in UTF-16 code units, as JavaScript counts it.

operator[]

Subscript operators

size

An object’s key count or an array’s length.

slice

Array.prototype.slice: a copy of the elements from start to stop, each a negative index counting from the end, clamped to the length.

stringify

The value as JSON.stringify(value, null, indent) writes it.

value_or

The value as T, or fallback: JavaScript’s v ?? fallback, and the fallback too when the value cannot be read as T.

values

Object.values: each value, in the policy’s order.

Friends

Name Description

webcpp::document::basic_parser

The streaming parser: write takes the text in chunks, finish returns the document.

webcpp::document::basic_value

A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.

webcpp::document::basic_const_value

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

webcpp::document::basic_document

A JSON document: one value, in one relocatable image.

webcpp::document::operator==

Whether two values are equal, structurally: the same kind; numbers by value (1, 1.0 and 1e0 are equal); strings, dates and bytes by value; arrays element by element; objects with the same keys, each with an equal value, in any order.

Derived Classes

Name Description

basic_value

A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

See Also

"Handles", in the guide.

webcpp::document::basic_const_value::basic_const_value

A handle that names nothing.

Synopsis

basic_const_value() noexcept = default;

webcpp::document::basic_const_value::as

Reads the value as T, as get does, throwing on failure; only with exceptions.

Synopsis

template<class T>
[[nodiscard]]
T
as() const
requires detail::exceptions_enabled<T> && detail::readable<Traits, T>;

Exceptions

Name Thrown on

type_error

For another kind.

range_error

For a number that does not fit.

Return Value

The value.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The type.

See Also

"The sugar that throws", in the guide.

webcpp::document::basic_const_value::as_array

The array as a vector-like container that reads: front, back, at, and iterators that <algorithm> and <ranges> take.

Synopsis

[[nodiscard]]
result<basic_array_ref<basic_const_value<Traits, Storage>>>
as_array() const noexcept;

Return Value

The view, defined in containers.hpp; errc::type for any other kind.

Note

The return value should not be discarded.

See Also

"Arrays", in the guide.

webcpp::document::basic_const_value::as_bool

Reads a boolean, throwing on failure; only with exceptions.

Synopsis

template<class B = bool>
[[nodiscard]]
bool
as_bool() const
requires detail::exceptions_enabled<B>;

Exceptions

Name Thrown on

type_error

For another kind.

Return Value

The boolean.

Note

The return value should not be discarded.

Template Parameters

Name Description

B

bool, a parameter only so that the call is checked where it is made.

webcpp::document::basic_const_value::as_object

The object as a sorted-map-like container that reads: find, bounds by key, and iterators that <algorithm> and <ranges> take.

Synopsis

[[nodiscard]]
result<basic_object_ref<basic_const_value<Traits, Storage>>>
as_object() const noexcept;

Return Value

The view, defined in containers.hpp; errc::type for any other kind.

Note

The return value should not be discarded.

See Also

"Key order", in the guide.

webcpp::document::basic_const_value::as_string

Reads a string as std::string_view, throwing on failure; only with exceptions.

Synopsis

template<class C = char>
[[nodiscard]]
std::string_view
as_string() const
requires detail::exceptions_enabled<C>;

Exceptions

Name Thrown on

type_error

For another kind.

Return Value

The string, valid until the document’s next change.

Note

The return value should not be discarded.

Template Parameters

Name Description

C

char, a parameter only so that the call is checked where it is made.

webcpp::document::basic_const_value::at

Array.prototype.at: the element index, a negative index counting from the end.

Synopsis

[[nodiscard]]
basic_const_value
at(int64_t index) const noexcept;

Return Value

Its handle; nothing out of range or for any other kind.

Note

The return value should not be discarded.

Parameters

Name Description

index

The index.

webcpp::document::basic_const_value::begin

The first element of an array, for for (auto v : arr).

Synopsis

[[nodiscard]]
/* implementation-defined */
begin() const noexcept;

Return Value

The iterator; the end for any other kind.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::concat

Array.prototype.concat: this array, then each argument, an array’s elements or the value.

Synopsis

template<class... T>
[[nodiscard]]
result<basic_document<Traits, owned>>
concat(T const&... v) const noexcept
requires (detail::settable<Traits, T> && ...);

Return Value

A new owned document holding the array; errc::type for any other kind, errc::capacity when it does not fit.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The arguments’ types, each settable.

Parameters

Name Description

v

The arguments.

webcpp::document::basic_const_value::contains

Whether an object has the member key, key in v.

Synopsis

[[nodiscard]]
bool
contains(std::string_view key) const noexcept;

Return Value

Whether it is there; false for any other kind.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_const_value::end

The end of an array’s elements.

Synopsis

[[nodiscard]]
/* implementation-defined */
end() const noexcept;

Return Value

The iterator.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::entries

Object.entries: pairs of each key and its value, in the policy’s order.

Synopsis

[[nodiscard]]
/* implementation-defined */
entries() const noexcept;

Return Value

The range; empty for any other kind than an object.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::get

Reads the value as T.

Synopsis

template<class T>
[[nodiscard]]
result<T>
get() const noexcept
requires detail::readable<Traits, T>;

Description

T is bool, any integer type, a decimal type, the traits’ number, std::string, std::string_view (with utf8), std::u16string, std::u16string_view (with utf16), date_ms, std::vector<std::byte> or std::span<const std::byte>. A view is valid until the document’s next change. A string reads as a date when it is exactly Date.prototype.toJSON’s text, and as bytes when it is text of the traits’ bytes policy.

Return Value

The value; errc::type when nothing is there or for another kind, errc::range for a number that is not an integer, does not fit T or that T cannot hold exactly.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The type.

webcpp::document::basic_const_value::is_array

Whether the value is an array, Array.isArray.

Synopsis

[[nodiscard]]
bool
is_array() const noexcept;

Return Value

Whether the kind is kind::array.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_boolean

Whether the value is true or false.

Synopsis

[[nodiscard]]
bool
is_boolean() const noexcept;

Return Value

Whether the kind is kind::boolean.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_bytes

Whether the value is bytes.

Synopsis

[[nodiscard]]
bool
is_bytes() const noexcept;

Return Value

Whether the kind is kind::bytes.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_date

Whether the value is a date.

Synopsis

[[nodiscard]]
bool
is_date() const noexcept;

Return Value

Whether the kind is kind::date.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_integer

Whether the value is a number that is an integer, JavaScript’s Number.isInteger.

Synopsis

[[nodiscard]]
bool
is_integer() const noexcept;

Return Value

Whether it is a number with no fractional part.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_null

Whether the value is null.

Synopsis

[[nodiscard]]
bool
is_null() const noexcept;

Return Value

Whether the kind is kind::null.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_number

Whether the value is a number.

Synopsis

[[nodiscard]]
bool
is_number() const noexcept;

Return Value

Whether the kind is kind::number.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_object

Whether the value is a plain object.

Synopsis

[[nodiscard]]
bool
is_object() const noexcept;

Return Value

Whether the kind is kind::object.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_string

Whether the value is a string.

Synopsis

[[nodiscard]]
bool
is_string() const noexcept;

Return Value

Whether the kind is kind::string.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::is_undefined

Whether nothing is there, v === undefined.

Synopsis

[[nodiscard]]
bool
is_undefined() const noexcept;

Return Value

Whether the kind is kind::undefined.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::keys

Object.keys: each key, in the policy’s order; a std::string_view with utf8, a std::u16string_view with utf16.

Synopsis

[[nodiscard]]
/* implementation-defined */
keys() const noexcept;

Return Value

The range; empty for any other kind than an object.

Note

The return value should not be discarded.

See Also

"Key order", in the guide.

webcpp::document::basic_const_value::kind

The kind of the value, JavaScript’s typeof with null, arrays, dates and bytes told apart.

Synopsis

[[nodiscard]]
webcpp::document::kind
kind() const noexcept;

Return Value

The kind; kind::undefined when nothing is there.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::length

An array’s length, or a string’s in UTF-16 code units, as JavaScript counts it.

Synopsis

[[nodiscard]]
result<uint64_t>
length() const noexcept;

Return Value

The length; errc::type for any other kind.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::operator[]

Subscript operators

Synopses

The member key, v.key, which never fails.

basic_const_value
operator[](std::string_view key) const noexcept;

The element index, v[index], which never fails.

basic_const_value
operator[](uint64_t index) const noexcept;

Return Value

  • Its handle: the member, a missing member of an object, or nothing.

  • Its handle: the element, a missing element of an array, or nothing.

Parameters

Name Description

key

The key, in UTF-8.

index

The index.

webcpp::document::basic_const_value::operator[]

The member key, v.key, which never fails.

Synopsis

basic_const_value
operator[](std::string_view key) const noexcept;

Return Value

Its handle: the member, a missing member of an object, or nothing.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_const_value::operator[]

The element index, v[index], which never fails.

Synopsis

basic_const_value
operator[](uint64_t index) const noexcept;

Return Value

Its handle: the element, a missing element of an array, or nothing.

Parameters

Name Description

index

The index.

webcpp::document::basic_const_value::size

An object’s key count or an array’s length.

Synopsis

[[nodiscard]]
uint64_t
size() const noexcept;

Return Value

The count; 0 for any other kind.

Note

The return value should not be discarded.

webcpp::document::basic_const_value::slice

Array.prototype.slice: a copy of the elements from start to stop, each a negative index counting from the end, clamped to the length.

Synopsis

[[nodiscard]]
result<basic_document<Traits, owned>>
slice(
    int64_t start = 0,
    int64_t stop = std::numeric_limits<std::int64_t>::max()) const noexcept;

Return Value

A new owned document holding the array of the copies; errc::type for any other kind, errc::capacity when it does not fit.

Note

The return value should not be discarded.

Parameters

Name Description

start

The first index.

stop

The index after the last.

webcpp::document::basic_const_value::stringify

The value as JSON.stringify(value, null, indent) writes it.

Synopsis

[[nodiscard]]
std::string
stringify(uint32_t indent = 0) const;

Return Value

The text; empty when the value is undefined.

Note

The return value should not be discarded.

Parameters

Name Description

indent

The spaces of each level, clamped to 10; 0 writes compactly.

webcpp::document::basic_const_value::value_or

The value as T, or fallback: JavaScript’s v ?? fallback, and the fallback too when the value cannot be read as T.

Synopsis

template<class T>
[[nodiscard]]
T
value_or(T fallback) const noexcept
requires detail::readable<Traits, T>;

Return Value

The value or the fallback.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The type, as get takes it.

Parameters

Name Description

fallback

The value when the value is undefined or null, or not readable as T.

webcpp::document::basic_const_value::values

Object.values: each value, in the policy’s order.

Synopsis

[[nodiscard]]
/* implementation-defined */
values() const noexcept;

Return Value

The range; empty for any other kind than an object.

Note

The return value should not be discarded.

webcpp::document::basic_document

A JSON document: one value, in one relocatable image.

Synopsis

template<
    class Traits = default_traits,
    class Storage = owned>
class basic_document;

Description

The image is a 64-byte header and a heap that holds the tree, every pointer in it an offset, so that it works at any 16-aligned address and has the same bytes on 64-bit native and wasm32. A document is moved cheaply; moving it, or destroying it, invalidates its handles, as a container’s iterators would be. It is copied only with exceptions, or by clone.

Member Functions

Name Description

basic_document [constructor]

Constructors

~basic_document [destructor]

Frees an owned image.

operator=

Assignment operators

capacity

The image’s size in bytes, which the document’s changes fill before an owned image grows and moves, and before an external one fails.

clone

Copies the image into a new owned document, as structuredClone would.

compact

Rebuilds an owned image as the compact image of its content: the nodes in document order, ids numbered from the first, each block right after the last with no hole between, the image exactly as large as they are.

compact_into

Writes the compact image of the document’s content into a buffer, as compact builds it, and opens it there: for a document that cannot reallocate, such as one leased from a pool or held in a shared block, or to send an image without waste.

compact_size

The size in bytes of the compact image of the document’s content, which compact and compact_into produce exactly: a program sizes a block with it.

image

The image: the header and the heap, which open and from_image read back.

operator[]

Subscript operators

reserve

Makes the image hold at least bytes, as std::vector::reserve: an owned image grows in one step, as a change that needs room grows it, to bytes or twice its size, the larger, at most its max_bytes.

root

root overloads

seal

Seals the image: writes into its header the CRC-32C of its bytes, which open, from_image and the image reader then check, refusing an image whose bytes changed.

sealed

Whether the image carries a seal, which seal wrote and no change has cleared since.

shrink_to_fit

Shrinks an owned image to the end of its last block in use, its content unchanged.

stringify

The document’s value as JSON.stringify(value, null, indent) writes it.

Static Member Functions

Name Description

create

Makes a new owned document, holding null.

Friends

Name Description

webcpp::document::basic_parser

The streaming parser: write takes the text in chunks, finish returns the document.

webcpp::document::basic_value

A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.

webcpp::document::basic_const_value

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

webcpp::document::basic_document

A JSON document: one value, in one relocatable image.

webcpp::document::operator==

Whether two documents hold equal values, structurally, key order aside.

Template Parameters

Name Description

Traits

The traits: the stored types, the encoding, the bytes policy and the key order.

Storage

owned or external.

See Also

"Handles", in the guide.

"Images", in the guide.

webcpp::document::basic_document::basic_document

Constructors

Synopses

Makes a new owned document, holding null, as create does, throwing on failure; only with exceptions.

basic_document()
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;

Copies a document’s image into a new owned document, as clone does, throwing on failure; only with exceptions.

basic_document(basic_document const& other)
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;

Takes the image of other, whose handles become invalid; other holds nothing after it.

basic_document(basic_document&& other) noexcept;

Exceptions

Name Thrown on

capacity_error

When even an empty document does not fit.

Parameters

Name Description

other

The document.

See Also

"The sugar that throws", in the guide.

webcpp::document::basic_document::basic_document

Makes a new owned document, holding null, as create does, throwing on failure; only with exceptions.

Synopsis

basic_document()
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;

Exceptions

Name Thrown on

capacity_error

When even an empty document does not fit.

See Also

"The sugar that throws", in the guide.

webcpp::document::basic_document::basic_document

Copies a document’s image into a new owned document, as clone does, throwing on failure; only with exceptions.

Synopsis

basic_document(basic_document const& other)
requires detail::exceptions_enabled<Traits> && std::same_as<Storage, owned>;

Exceptions

Name Thrown on

capacity_error

When the copy does not fit.

Parameters

Name Description

other

The document.

webcpp::document::basic_document::basic_document

Takes the image of other, whose handles become invalid; other holds nothing after it.

Synopsis

basic_document(basic_document&& other) noexcept;

Parameters

Name Description

other

The document.

webcpp::document::basic_document::~basic_document

Frees an owned image.

Synopsis

~basic_document() = default;

webcpp::document::basic_document::operator=

Assignment operators

Synopses

A document is copied by clone, or with exceptions by its copy constructor.

Takes the image of other, freeing this document’s own; the handles of both become invalid.

Return Value

This document.

Parameters

Name Description

other

The document.

webcpp::document::basic_document::operator=

A document is copied by clone, or with exceptions by its copy constructor.

Synopsis

basic_document&
operator=(basic_document const&) = delete;

webcpp::document::basic_document::operator=

Takes the image of other, freeing this document’s own; the handles of both become invalid.

Synopsis

basic_document&
operator=(basic_document&& other) noexcept;

Return Value

This document.

Parameters

Name Description

other

The document.

webcpp::document::basic_document::capacity

The image’s size in bytes, which the document’s changes fill before an owned image grows and moves, and before an external one fails.

Synopsis

[[nodiscard]]
uint64_t
capacity() const noexcept;

Return Value

The size, as image gives it; 0 for a moved-from document.

Note

The return value should not be discarded.

See Also

"Images", in the guide.

webcpp::document::basic_document::clone

Copies the image into a new owned document, as structuredClone would.

Synopsis

[[nodiscard]]
result<basic_document<Traits, owned>>
clone() const noexcept;

Description

The copy keeps a seal, its bytes being the image’s; the seal is not checked, the image being this document’s own.

Return Value

The copy, equal and independent; errc::capacity when it does not fit, and errc::type for a moved-from document, which has no image.

Note

The return value should not be discarded.

webcpp::document::basic_document::compact

Rebuilds an owned image as the compact image of its content: the nodes in document order, ids numbered from the first, each block right after the last with no hole between, the image exactly as large as they are.

Synopsis

result<void>
compact() noexcept
requires std::same_as<Storage, owned>;

Description

Two documents of equal content, whatever edits made them, have byte-identical images after it, and a compact image compacts to itself. The copy is built before it replaces the image, so a failure changes nothing. As a vector’s reallocation does, it invalidates every handle, every std::string_view read from a value and every iterator, and, the bytes having changed, it clears a seal: sealing after it is the caller’s. The image’s size is compact_size afterwards, which is also its capacity, so the next change that needs room grows it.

Return Value

Success; errc::capacity when the new buffer cannot be allocated, and errc::type for a moved-from document.

See Also

"Images", in the guide.

webcpp::document::basic_document::compact_into

Writes the compact image of the document’s content into a buffer, as compact builds it, and opens it there: for a document that cannot reallocate, such as one leased from a pool or held in a shared block, or to send an image without waste.

Synopsis

[[nodiscard]]
result<basic_document<Traits, external>>
compact_into(std::span<std::byte> buffer) const noexcept;

Description

The buffer is zeroed past the image. This document is not changed.

Return Value

The document, over the buffer’s first compact_size bytes; errc::capacity when the buffer is smaller or is not 16-aligned, writing nothing; errc::type for a moved-from document and for a buffer that overlaps the image.

Note

The return value should not be discarded.

Parameters

Name Description

buffer

The buffer, 16-aligned, of at least compact_size bytes, which the returned document uses in place and its user keeps alive; it must not overlap this document’s image.

See Also

"Images", in the guide.

webcpp::document::basic_document::compact_size

The size in bytes of the compact image of the document’s content, which compact and compact_into produce exactly: a program sizes a block with it.

Synopsis

[[nodiscard]]
uint64_t
compact_size() const noexcept;

Description

It reads every node and allocates nothing, a fraction of the cost of the copy.

Return Value

The size; 0 for a moved-from document.

Note

The return value should not be discarded.

See Also

"Images", in the guide.

webcpp::document::basic_document::image

The image: the header and the heap, which open and from_image read back.

Synopsis

[[nodiscard]]
std::span<std::byte const>
image() const noexcept;

Return Value

Its bytes, valid until the document’s next change.

Note

The return value should not be discarded.

webcpp::document::basic_document::operator[]

Subscript operators

Synopses

The member key of the document’s value, as root()[key].

basic_value<Traits, Storage>
operator[](std::string_view key) noexcept;

The member key of the document’s value, read only.

basic_const_value<Traits, Storage>
operator[](std::string_view key) const noexcept;

The element index of the document’s value, as root()[index].

basic_value<Traits, Storage>
operator[](uint64_t index) noexcept;

The element index of the document’s value, read only.

basic_const_value<Traits, Storage>
operator[](uint64_t index) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

index

The index.

webcpp::document::basic_document::operator[]

The member key of the document’s value, as root()[key].

Synopsis

basic_value<Traits, Storage>
operator[](std::string_view key) noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_document::operator[]

The member key of the document’s value, read only.

Synopsis

basic_const_value<Traits, Storage>
operator[](std::string_view key) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_document::operator[]

The element index of the document’s value, as root()[index].

Synopsis

basic_value<Traits, Storage>
operator[](uint64_t index) noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

index

The index.

webcpp::document::basic_document::operator[]

The element index of the document’s value, read only.

Synopsis

basic_const_value<Traits, Storage>
operator[](uint64_t index) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

index

The index.

webcpp::document::basic_document::reserve

Makes the image hold at least bytes, as std::vector::reserve: an owned image grows in one step, as a change that needs room grows it, to bytes or twice its size, the larger, at most its max_bytes.

Synopsis

[[nodiscard]]
result<void>
reserve(uint64_t bytes) noexcept;

Description

Growing moves the image, which invalidates every std::string_view read from a value but no handle or iterator, and clears a seal; a reservation within the capacity changes nothing. Building a large document after one reservation of its size moves the image once, not at each doubling.

Return Value

Success; errc::capacity when bytes is past max_bytes, or past an external image’s size, which never grows, or the larger buffer cannot be allocated, each changing nothing; errc::type for a moved-from document.

Note

The return value should not be discarded.

Parameters

Name Description

bytes

The size the image must reach.

See Also

"Images", in the guide.

webcpp::document::basic_document::root

root overloads

Synopses

The handle of the document’s value.

The handle of the document’s value, read only.

[[nodiscard]]
basic_const_value<Traits, Storage>
root() const noexcept;

Return Value

  • The handle, which reads and writes.

  • The handle.

webcpp::document::basic_document::root

The handle of the document’s value.

Synopsis

basic_value<Traits, Storage>
root() noexcept;

Return Value

The handle, which reads and writes.

webcpp::document::basic_document::root

The handle of the document’s value, read only.

Synopsis

[[nodiscard]]
basic_const_value<Traits, Storage>
root() const noexcept;

Return Value

The handle.

Note

The return value should not be discarded.

webcpp::document::basic_document::seal

Seals the image: writes into its header the CRC-32C of its bytes, which open, from_image and the image reader then check, refusing an image whose bytes changed.

Synopsis

result<void>
seal() noexcept;

Description

The seal holds until the document’s next change, which clears it: the image is then simply not sealed, never wrongly sealed. It catches bytes changed inside an image (a flipped bit on a disk or a wire, a torn write, a stray write), not an old but valid image in reused memory, which carries a valid seal of its own. It costs a pass over the image, Boost.CRC’s table a byte at a time: measured natively at -O2 on one core of an Apple M5 Max, 1.7 ms per MiB (about 600 MB/s), and the check when an image is opened costs the same.

Return Value

Success; errc::type for a moved-from document, which has no image.

See Also

"Images", in the guide.

webcpp::document::basic_document::sealed

Whether the image carries a seal, which seal wrote and no change has cleared since.

Synopsis

[[nodiscard]]
bool
sealed() const noexcept;

Return Value

Whether it is sealed; false for a moved-from document. The seal is not checked here.

Note

The return value should not be discarded.

webcpp::document::basic_document::shrink_to_fit

Shrinks an owned image to the end of its last block in use, its content unchanged.

Synopsis

result<void>
shrink_to_fit() noexcept
requires std::same_as<Storage, owned>;

Description

Blocks never move, so the free blocks before the last one in use stay in the image: a document that grew and then let most of its values go may stay nearly as large. A deep copy into a new document, packed.root().set(doc), packs the content, and compact packs it in one order whatever the document’s history.

Return Value

Success; errc::capacity when the smaller buffer cannot be allocated.

webcpp::document::basic_document::stringify

The document’s value as JSON.stringify(value, null, indent) writes it.

Synopsis

[[nodiscard]]
std::string
stringify(uint32_t indent = 0) const;

Return Value

The text; empty when the document holds undefined.

Note

The return value should not be discarded.

Parameters

Name Description

indent

The spaces of each level, clamped to 10; 0 writes compactly.

webcpp::document::basic_document::create

Makes a new owned document, holding null.

Synopsis

static
result<basic_document>
create(uint64_t max_bytes = default_max_bytes) noexcept
requires std::same_as<Storage, owned>;

Return Value

The document; errc::capacity when even an empty one does not fit max_bytes.

Parameters

Name Description

max_bytes

The size its image may grow to; past it an operation returns errc::capacity.

webcpp::document::basic_image_reader

Reads a stream of images, one after another with nothing between them, into the blocks of an image_pool, and opens each in place as a leased_document.

Synopsis

template<class Traits = default_traits>
class basic_image_reader;

Description

Sans-IO: the bytes come from any source, copied by write, or read straight into the block by the source itself, through prepare and commit. The first 64 bytes of each image are checked as its header before a byte more is taken: its magic, format version, byte order, reserved bytes, the layout tag of Traits, and a size within a block. The rest of the image follows into the same block, and the image is opened there, a sealed one checked against its seal, and its heap checked. A refusal stops the reader with its error until reset, since the stream has lost its framing; the documents completed before it are still delivered.

The documents completed and not yet taken are queued in their own blocks, so the reader allocates nothing. When every block of the pool is leased, the reader takes no more bytes: that is the stream’s backpressure.

Note

Images are trusted input: this path is for peers the program trusts, and untrusted data enters as JSON text.

Member Functions

Name Description

basic_image_reader [constructor]

Constructors

~basic_image_reader [destructor]

Gives back the blocks of the image being read and of the documents not taken.

operator=

Assignment operators

commit

Takes bytes bytes written at the start of the room prepare gave: the header is checked once it is whole, and the image opened once it is.

documents

How many images the reader has completed and opened.

finish

Ends the stream: an image cut short is an error, and a stream that ends between two images is whole.

next

The oldest document completed and not yet taken.

offset

How many bytes of the stream the reader has taken.

prepare

The room the stream’s next bytes go into, in a block: the rest of the image’s header, or the rest of the image it states, so that a read never crosses an image’s end.

reset

Makes the reader new: gives back every block it holds, its documents not taken included.

write

write overloads

Template Parameters

Name Description

Traits

The traits of the images; another traits’ image is refused.

See Also

"Images", in the guide.

webcpp::document::basic_image_reader::basic_image_reader

Constructors

Synopses

A reader is not copied.

Takes the stream of other, its documents and its block, leaving other a new reader on the same pool.

A reader whose images go into the blocks of pool.

explicit
basic_image_reader(image_pool& pool) noexcept;

Parameters

Name Description

other

The reader.

pool

The pool, which outlives the reader and its documents.

webcpp::document::basic_image_reader::basic_image_reader

A reader is not copied.

Synopsis

basic_image_reader(basic_image_reader const&) = delete;

webcpp::document::basic_image_reader::basic_image_reader

Takes the stream of other, its documents and its block, leaving other a new reader on the same pool.

Synopsis

basic_image_reader(basic_image_reader&& other) noexcept;

Parameters

Name Description

other

The reader.

webcpp::document::basic_image_reader::basic_image_reader

A reader whose images go into the blocks of pool.

Synopsis

explicit
basic_image_reader(image_pool& pool) noexcept;

Parameters

Name Description

pool

The pool, which outlives the reader and its documents.

webcpp::document::basic_image_reader::~basic_image_reader

Gives back the blocks of the image being read and of the documents not taken.

Synopsis

~basic_image_reader();

webcpp::document::basic_image_reader::operator=

Assignment operators

Synopses

A reader is not copied.

Gives back this reader’s blocks, then takes the stream of other, its documents and its block, leaving other a new reader on the same pool.

Return Value

This reader.

Parameters

Name Description

other

The reader.

webcpp::document::basic_image_reader::operator=

A reader is not copied.

Synopsis

basic_image_reader&
operator=(basic_image_reader const&) = delete;

webcpp::document::basic_image_reader::operator=

Gives back this reader’s blocks, then takes the stream of other, its documents and its block, leaving other a new reader on the same pool.

Synopsis

basic_image_reader&
operator=(basic_image_reader&& other) noexcept;

Return Value

This reader.

Parameters

Name Description

other

The reader.

webcpp::document::basic_image_reader::commit

Takes bytes bytes written at the start of the room prepare gave: the header is checked once it is whole, and the image opened once it is.

Synopsis

result<std::size_t>
commit(std::size_t bytes) noexcept;

Return Value

How many documents the bytes completed, 0 or 1; errc::range for more bytes than the room holds, which changes nothing; the error that stops the reader: errc::not_a_document for bytes that are not an image’s or an image whose heap is not sound, errc::incompatible_document for an image of another format version, byte order, reserved bytes that are not zero or traits, errc::damaged_document for a sealed image whose bytes do not match its seal, and errc::capacity for an image larger than a block.

Parameters

Name Description

bytes

How many bytes were written, at most the room’s size.

webcpp::document::basic_image_reader::documents

How many images the reader has completed and opened.

Synopsis

[[nodiscard]]
uint64_t
documents() const noexcept;

Return Value

The count, since the reader was made or reset, the documents not yet taken included.

Note

The return value should not be discarded.

webcpp::document::basic_image_reader::finish

Ends the stream: an image cut short is an error, and a stream that ends between two images is whole.

Synopsis

result<void>
finish() noexcept;

Return Value

Success; errc::not_a_document for an image cut short, which stops the reader; the error that stopped it.

webcpp::document::basic_image_reader::next

The oldest document completed and not yet taken.

Return Value

The document, or nothing when none is waiting; once the documents completed before an error are taken, the error that stopped the reader.

webcpp::document::basic_image_reader::offset

How many bytes of the stream the reader has taken.

Synopsis

[[nodiscard]]
uint64_t
offset() const noexcept;

Return Value

The count, since the reader was made or reset.

Note

The return value should not be discarded.

webcpp::document::basic_image_reader::prepare

The room the stream’s next bytes go into, in a block: the rest of the image’s header, or the rest of the image it states, so that a read never crosses an image’s end.

Synopsis

result<std::span<std::byte>>
prepare() noexcept;

Description

The source writes some of the bytes there and gives their count to commit, so that a socket reads straight into the block, with no copy.

Return Value

The room, never empty; errc::capacity when a new image needs a block and every block is leased, which changes nothing; the error that stopped the reader.

webcpp::document::basic_image_reader::reset

Makes the reader new: gives back every block it holds, its documents not taken included.

Synopsis

void
reset() noexcept;

webcpp::document::basic_image_reader::write

write overloads

Synopses

Copies the stream’s next bytes into blocks, opening each image they complete.

result<std::size_t>
write(std::span<std::byte const> bytes) noexcept;

Copies the stream’s next bytes into blocks, and gives on_document each document, in order, the documents not yet taken first: as soon as it returns, the block of a document it let go is free for the next image.

template<class F>
requires std::invocable<F&, leased_document<Traits>&&>
result<std::size_t>
write(
    std::span<std::byte const> bytes,
    F&& on_document);

Return Value

  • How many bytes were taken: all of them, or fewer when every block of the pool is leased, the rest to be written again once a block is given back; the error that stops the reader, as commit returns it.

  • As write without a callable returns.

Template Parameters

Name Description

F

A callable taking a leased_document<Traits>&&.

Parameters

Name Description

bytes

The next bytes of the stream.

on_document

The callable.

webcpp::document::basic_image_reader::write

Copies the stream’s next bytes into blocks, opening each image they complete.

Synopsis

result<std::size_t>
write(std::span<std::byte const> bytes) noexcept;

Return Value

How many bytes were taken: all of them, or fewer when every block of the pool is leased, the rest to be written again once a block is given back; the error that stops the reader, as commit returns it.

Parameters

Name Description

bytes

The next bytes of the stream.

webcpp::document::basic_image_reader::write

Copies the stream’s next bytes into blocks, and gives on_document each document, in order, the documents not yet taken first: as soon as it returns, the block of a document it let go is free for the next image.

Synopsis

template<class F>
requires std::invocable<F&, leased_document<Traits>&&>
result<std::size_t>
write(
    std::span<std::byte const> bytes,
    F&& on_document);

Return Value

As write without a callable returns.

Template Parameters

Name Description

F

A callable taking a leased_document<Traits>&&.

Parameters

Name Description

bytes

The next bytes of the stream.

on_document

The callable.

webcpp::document::basic_object_by_key

The members of an object in lexicographic key order, by UTF-16 code units whatever the encoding, with find, lower_bound, upper_bound and equal_range: prefix and range queries over keys.

Synopsis

template<class Handle>
class basic_object_by_key;

Description

It reads the tree’s index by key, so each step and each bound costs O(log n). Under the sorted policy it is also the object’s own order. It is a std::ranges::bidirectional_range and a borrowed one, made by basic_object_ref::by_key; its iterators are invalidated as basic_object_ref’s are.

Type Aliases

Name Description

difference_type

The difference of two positions.

iterator

A bidirectional iterator in key order.

reference

What dereferencing an iterator gives: a value, made at each step.

reverse_iterator

A reverse iterator in key order.

size_type

A count of members.

storage_type

The document’s storage.

traits_type

The document’s traits.

value_type

A member: its key, a view of the image valid until the next change, and its handle.

Member Functions

Name Description

basic_object_by_key [constructor]

A view of nothing, which is empty.

begin

The member whose key is first in key order.

empty

Whether the object has no member.

end

The end of the members.

equal_range

The members whose key is key, as std::map::equal_range: one member or none.

find

The member key, in O(log n).

lower_bound

The first member whose key is not before key, as std::map::lower_bound: with upper_bound or another lower_bound, the bounds of a range of keys, such as those with a prefix.

rbegin

The member whose key is last in key order, first in reverse.

rend

The end of the members in reverse.

size

How many members the object has.

upper_bound

The first member whose key is after key, as std::map::upper_bound.

Friends

Name Description

webcpp::document::basic_object_ref

An object as a sorted-map-like container, in the traits’ key order (JavaScript’s by default): find, insert, insert_or_assign, try_emplace, erase, iteration both ways, and by_key() for prefix and range queries in lexicographic key order.

Template Parameters

Name Description

Handle

The handle its members are read through, basic_value or basic_const_value.

See Also

"Key order", in the guide.

webcpp::document::basic_object_by_key::difference_type

The difference of two positions.

Synopsis

using difference_type = std::ptrdiff_t;

webcpp::document::basic_object_by_key::iterator

A bidirectional iterator in key order.

Synopsis

using iterator = /* implementation-defined */;

webcpp::document::basic_object_by_key::reference

What dereferencing an iterator gives: a value, made at each step.

Synopsis

using reference = value_type;

webcpp::document::basic_object_by_key::reverse_iterator

A reverse iterator in key order.

Synopsis

using reverse_iterator = std::reverse_iterator<iterator>;

webcpp::document::basic_object_by_key::size_type

A count of members.

Synopsis

using size_type = uint64_t;

webcpp::document::basic_object_by_key::storage_type

The document’s storage.

Synopsis

using storage_type = /* implementation-defined */::storage;

webcpp::document::basic_object_by_key::traits_type

The document’s traits.

Synopsis

using traits_type = /* implementation-defined */::traits;

webcpp::document::basic_object_by_key::value_type

A member: its key, a view of the image valid until the next change, and its handle.

Synopsis

using value_type = std::pair</* implementation-defined */, Handle>;

webcpp::document::basic_object_by_key::basic_object_by_key

A view of nothing, which is empty.

Synopsis

basic_object_by_key() noexcept = default;

webcpp::document::basic_object_by_key::begin

The member whose key is first in key order.

Synopsis

[[nodiscard]]
iterator
begin() const noexcept;

Return Value

The iterator; the end when there is none.

Note

The return value should not be discarded.

webcpp::document::basic_object_by_key::empty

Whether the object has no member.

Synopsis

[[nodiscard]]
bool
empty() const noexcept;

Return Value

Whether size is 0.

Note

The return value should not be discarded.

webcpp::document::basic_object_by_key::end

The end of the members.

Synopsis

[[nodiscard]]
iterator
end() const noexcept;

Return Value

The iterator.

Note

The return value should not be discarded.

webcpp::document::basic_object_by_key::equal_range

The members whose key is key, as std::map::equal_range: one member or none.

Synopsis

[[nodiscard]]
std::pair<iterator, iterator>
equal_range(std::string_view key) const noexcept;

Return Value

The pair of lower_bound and upper_bound.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_object_by_key::find

The member key, in O(log n).

Synopsis

[[nodiscard]]
iterator
find(std::string_view key) const noexcept;

Return Value

Its iterator; the end when it is not there.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_object_by_key::lower_bound

The first member whose key is not before key, as std::map::lower_bound: with upper_bound or another lower_bound, the bounds of a range of keys, such as those with a prefix.

Synopsis

[[nodiscard]]
iterator
lower_bound(std::string_view key) const noexcept;

Return Value

The iterator; the end when every key is before key, or when key is not valid UTF-8.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8; it need not be a member’s.

webcpp::document::basic_object_by_key::rbegin

The member whose key is last in key order, first in reverse.

Synopsis

[[nodiscard]]
reverse_iterator
rbegin() const noexcept;

Return Value

The reverse iterator.

Note

The return value should not be discarded.

webcpp::document::basic_object_by_key::rend

The end of the members in reverse.

Synopsis

[[nodiscard]]
reverse_iterator
rend() const noexcept;

Return Value

The reverse iterator.

Note

The return value should not be discarded.

webcpp::document::basic_object_by_key::size

How many members the object has.

Synopsis

[[nodiscard]]
size_type
size() const noexcept;

Return Value

The count; 0 once it is no object.

Note

The return value should not be discarded.

webcpp::document::basic_object_by_key::upper_bound

The first member whose key is after key, as std::map::upper_bound.

Synopsis

[[nodiscard]]
iterator
upper_bound(std::string_view key) const noexcept;

Return Value

The iterator; the end when no key is after key, or when key is not valid UTF-8.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8; it need not be a member’s.

webcpp::document::basic_object_ref

An object as a sorted-map-like container, in the traits’ key order (JavaScript’s by default): find, insert, insert_or_assign, try_emplace, erase, iteration both ways, and by_key() for prefix and range queries in lexicographic key order.

Synopsis

template<class Handle>
class basic_object_ref;

Description

Made by as_object() on a handle, it holds the document’s address and the object’s id, and is cheap to copy: a reference, like std::span, whose constness is its own, so a const view still writes; const_object_ref is the view that only reads. It is a std::ranges::bidirectional_range, a view and a borrowed range, so <algorithm> and <ranges> work on it, and an iterator outlives the view that made it. Its iterators are bidirectional, not random access: each step finds the next member in O(log n), so a jump of n costs n steps.

Iterators and handles hold ids, which no change reuses:

| Change | Iterators and handles | A std::string_view read from a value | | --- | --- | --- | | A member inserted, and any growth of the image | all stay valid | invalid | | A member erased | those to it and its subtree, which read undefined | invalid | | A member’s value replaced | those to its old subtree | invalid | | The document moved from or destroyed | all | invalid |

What can fail returns result: an insertion, which needs room in the image. A lookup returns an iterator, the end when nothing is found. There are no throwing forms: they would differ from these only in their return type, which C++ does not overload on.

Type Aliases

Name Description

difference_type

The difference of two positions.

iterator

A bidirectional iterator in the traits’ key order.

key_type

A key as the members take it: UTF-8, whatever the traits’ encoding.

mapped_type

A member’s value: its handle.

reference

What dereferencing an iterator gives: a value, made at each step.

reverse_iterator

A reverse iterator in the traits’ key order.

size_type

A count of members.

storage_type

The document’s storage.

traits_type

The document’s traits.

value_type

A member: its key, a view of the image valid until the next change, and its handle.

Member Functions

Name Description

basic_object_ref [constructor]

A view of nothing, which is empty and fails every insertion with errc::type.

begin

The first member in the traits’ key order.

by_key

The members in lexicographic key order, by UTF-16 code units, with bounds for prefix and range queries.

clear

Erases every member, which needs no room.

contains

Whether the object has the member key, key in o.

count

How many members have the key key: 1 or 0, as std::map::count.

empty

Whether the object has no member.

end

The end of the members, which every change leaves valid.

erase

erase overloads

find

The member key, in O(log n).

insert

Inserts the member key holding v unless it is there, as std::map::insert: a new key takes the place the traits’ policy gives it, and an existing one is left as it is.

insert_or_assign

Inserts the member key holding v, or assigns v to it when it is there, where it keeps its place: JavaScript’s o[key]= v.

rbegin

The last member in the traits’ key order, first in reverse.

rend

The end of the members in reverse.

size

How many members the object has, Object.keys(o).length.

try_emplace

Inserts the member key holding v unless it is there, as std::map::try_emplace.

Friends

Name Description

webcpp::document::basic_value

A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.

webcpp::document::basic_const_value

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

Template Parameters

Name Description

Handle

The handle its members are read through: basic_value, to write, or basic_const_value.

See Also

"Handles", in the guide.

"Key order", in the guide.

webcpp::document::basic_object_ref::difference_type

The difference of two positions.

Synopsis

using difference_type = std::ptrdiff_t;

webcpp::document::basic_object_ref::iterator

A bidirectional iterator in the traits’ key order.

Synopsis

using iterator = /* implementation-defined */;

webcpp::document::basic_object_ref::key_type

A key as the members take it: UTF-8, whatever the traits’ encoding.

Synopsis

using key_type = std::string_view;

webcpp::document::basic_object_ref::mapped_type

A member’s value: its handle.

Synopsis

using mapped_type = Handle;

webcpp::document::basic_object_ref::reference

What dereferencing an iterator gives: a value, made at each step.

Synopsis

using reference = value_type;

webcpp::document::basic_object_ref::reverse_iterator

A reverse iterator in the traits’ key order.

Synopsis

using reverse_iterator = std::reverse_iterator<iterator>;

webcpp::document::basic_object_ref::size_type

A count of members.

Synopsis

using size_type = uint64_t;

webcpp::document::basic_object_ref::storage_type

The document’s storage.

Synopsis

using storage_type = /* implementation-defined */::storage;

webcpp::document::basic_object_ref::traits_type

The document’s traits.

Synopsis

using traits_type = /* implementation-defined */::traits;

webcpp::document::basic_object_ref::value_type

A member: its key, a view of the image valid until the next change, and its handle.

Synopsis

using value_type = std::pair</* implementation-defined */, Handle>;

webcpp::document::basic_object_ref::basic_object_ref

A view of nothing, which is empty and fails every insertion with errc::type.

Synopsis

basic_object_ref() noexcept = default;

webcpp::document::basic_object_ref::begin

The first member in the traits’ key order.

Synopsis

[[nodiscard]]
iterator
begin() const noexcept;

Return Value

The iterator; the end when there is none.

Note

The return value should not be discarded.

webcpp::document::basic_object_ref::by_key

The members in lexicographic key order, by UTF-16 code units, with bounds for prefix and range queries.

Synopsis

[[nodiscard]]
basic_object_by_key<Handle>
by_key() const noexcept;

Return Value

The view.

Note

The return value should not be discarded.

webcpp::document::basic_object_ref::clear

Erases every member, which needs no room.

Synopsis

void
clear() const noexcept
requires detail::writable_handle<Handle>;

webcpp::document::basic_object_ref::contains

Whether the object has the member key, key in o.

Synopsis

[[nodiscard]]
bool
contains(std::string_view key) const noexcept;

Return Value

Whether it is there.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_object_ref::count

How many members have the key key: 1 or 0, as std::map::count.

Synopsis

[[nodiscard]]
size_type
count(std::string_view key) const noexcept;

Return Value

The count.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_object_ref::empty

Whether the object has no member.

Synopsis

[[nodiscard]]
bool
empty() const noexcept;

Return Value

Whether size is 0.

Note

The return value should not be discarded.

webcpp::document::basic_object_ref::end

The end of the members, which every change leaves valid.

Synopsis

[[nodiscard]]
iterator
end() const noexcept;

Return Value

The iterator.

Note

The return value should not be discarded.

webcpp::document::basic_object_ref::erase

erase overloads

Synopses

Erases the member at pos and its subtree, delete o[key].

iterator
erase(iterator pos) const noexcept
requires detail::writable_handle<Handle>;

Erases the member key and its subtree, delete o[key].

[[nodiscard]]
result<size_type>
erase(std::string_view key) const noexcept
requires detail::writable_handle<Handle>;

Erases the members from first to last, last excluded, and their subtrees.

iterator
erase(
    iterator first,
    iterator last) const noexcept
requires detail::writable_handle<Handle>;

Return Value

  • The iterator of the member after it; the end when pos is the end, of another container, or at a member already erased.

  • How many members were erased, 1 or 0; errc::type once this is no object.

  • last.

Parameters

Name Description

pos

An iterator of this object at a member.

key

The key, in UTF-8.

first

An iterator of this object.

last

An iterator of this object at or after first.

webcpp::document::basic_object_ref::erase

Erases the member at pos and its subtree, delete o[key].

Synopsis

iterator
erase(iterator pos) const noexcept
requires detail::writable_handle<Handle>;

Return Value

The iterator of the member after it; the end when pos is the end, of another container, or at a member already erased.

Parameters

Name Description

pos

An iterator of this object at a member.

webcpp::document::basic_object_ref::erase

Erases the member key and its subtree, delete o[key].

Synopsis

[[nodiscard]]
result<size_type>
erase(std::string_view key) const noexcept
requires detail::writable_handle<Handle>;

Return Value

How many members were erased, 1 or 0; errc::type once this is no object.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_object_ref::erase

Erases the members from first to last, last excluded, and their subtrees.

Synopsis

iterator
erase(
    iterator first,
    iterator last) const noexcept
requires detail::writable_handle<Handle>;

Return Value

last.

Parameters

Name Description

first

An iterator of this object.

last

An iterator of this object at or after first.

webcpp::document::basic_object_ref::find

The member key, in O(log n).

Synopsis

[[nodiscard]]
iterator
find(std::string_view key) const noexcept;

Return Value

Its iterator; the end when it is not there.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_object_ref::insert

Inserts the member key holding v unless it is there, as std::map::insert: a new key takes the place the traits’ policy gives it, and an existing one is left as it is.

Synopsis

template<class T>
[[nodiscard]]
result<std::pair<iterator, bool>>
insert(
    std::string_view key,
    T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;

Return Value

The member’s iterator and whether it was inserted; errc::type once this is no object, for a key that is not valid UTF-8 and for a source that reads undefined, errc::capacity when there is no room, and the errors of set for the value.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The value’s type, any that set takes but undefined.

Parameters

Name Description

key

The key, in UTF-8.

v

The value; a handle or a document is copied deeply.

webcpp::document::basic_object_ref::insert_or_assign

Inserts the member key holding v, or assigns v to it when it is there, where it keeps its place: JavaScript’s o[key]= v.

Synopsis

template<class T>
[[nodiscard]]
result<std::pair<iterator, bool>>
insert_or_assign(
    std::string_view key,
    T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;

Return Value

The member’s iterator and whether it was inserted; the errors of insert.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The value’s type, any that set takes but undefined.

Parameters

Name Description

key

The key, in UTF-8.

v

The value; a handle or a document is copied deeply.

webcpp::document::basic_object_ref::rbegin

The last member in the traits’ key order, first in reverse.

Synopsis

[[nodiscard]]
reverse_iterator
rbegin() const noexcept;

Return Value

The reverse iterator.

Note

The return value should not be discarded.

webcpp::document::basic_object_ref::rend

The end of the members in reverse.

Synopsis

[[nodiscard]]
reverse_iterator
rend() const noexcept;

Return Value

The reverse iterator.

Note

The return value should not be discarded.

webcpp::document::basic_object_ref::size

How many members the object has, Object.keys(o).length.

Synopsis

[[nodiscard]]
size_type
size() const noexcept;

Return Value

The count; 0 once it is no object.

Note

The return value should not be discarded.

webcpp::document::basic_object_ref::try_emplace

Inserts the member key holding v unless it is there, as std::map::try_emplace.

Synopsis

template<class T>
[[nodiscard]]
result<std::pair<iterator, bool>>
try_emplace(
    std::string_view key,
    T const& v) const noexcept
requires detail::writable_handle<Handle> && detail::settable<traits_type, T>;

Description

It is insert, since neither builds a value before the key is known to be absent, so v is read only when it is stored.

Return Value

The member’s iterator and whether it was inserted; the errors of insert.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The value’s type, any that set takes but undefined.

Parameters

Name Description

key

The key, in UTF-8.

v

The value; a handle or a document is copied deeply.

webcpp::document::basic_parser

The streaming parser: write takes the text in chunks, finish returns the document.

Synopsis

template<
    class Traits = default_traits,
    class Storage = owned>
class basic_parser;

Description

Boost.JSON’s basic_parser is fed each chunk, and its handler builds the document node by node, by ids, so that the document may grow and move between chunks. A failed write leaves the parser failed: every later call returns the same error, until reset. The parser raises nothing of its own, since its input is the network’s; an exception a callback throws propagates from write or finish, and leaves the parser failed: every later call throws it again, until reset.

Member Functions

Name Description

basic_parser [constructor]

Constructors

~basic_parser [destructor]

Destroys the parser and the document it was building.

operator=

Assignment operators

cause

The code of Boost.JSON behind an errc::syntax.

finish

Ends the text and returns the document, without the values on took.

observe

Delivers each value whose path matches selector once it completes, and keeps it.

offset

How many bytes of the text were consumed.

on

Delivers each value whose path matches selector once it completes, then removes it from the document, its blocks zeroed and freed: a stream of a million elements needs the memory of one.

reset

Makes the parser ready for another text, its selectors kept.

write

Feeds the next chunk of the text, delivering each value a selector matches as it completes.

Friends

Name Description

webcpp::document::basic_stream_parser

The parser of a stream of documents: write takes the bytes in chunks of any size, down to one byte, and each document is delivered as soon as the bytes that end it are consumed.

Template Parameters

Name Description

Traits

The traits of the document.

Storage

owned, or external to build it in a buffer of its user’s.

See Also

"Parsing", in the guide.

webcpp::document::basic_parser::basic_parser

Constructors

Synopses

A parser is not copied: its handler and its document refer to it.

basic_parser(basic_parser const&) = delete;

A parser is not moved: its handler and its document refer to it.

basic_parser(basic_parser&&) = delete;

A parser of an owned document.

explicit
basic_parser(parse_options const& o = {}) noexcept
requires std::same_as<Storage, owned>;

A parser of a document in a buffer its user owns, formatted when the text begins.

explicit
basic_parser(
    std::span<std::byte> buffer,
    parse_options const& o = {}) noexcept
requires std::same_as<Storage, external>;

Parameters

Name Description

o

The limits and the revivers.

buffer

The buffer, 16-aligned, which outlives the parser and its document.

webcpp::document::basic_parser::basic_parser

A parser is not copied: its handler and its document refer to it.

Synopsis

basic_parser(basic_parser const&) = delete;

webcpp::document::basic_parser::basic_parser

A parser is not moved: its handler and its document refer to it.

Synopsis

basic_parser(basic_parser&&) = delete;

webcpp::document::basic_parser::basic_parser

A parser of an owned document.

Synopsis

explicit
basic_parser(parse_options const& o = {}) noexcept
requires std::same_as<Storage, owned>;

Parameters

Name Description

o

The limits and the revivers.

webcpp::document::basic_parser::basic_parser

A parser of a document in a buffer its user owns, formatted when the text begins.

Synopsis

explicit
basic_parser(
    std::span<std::byte> buffer,
    parse_options const& o = {}) noexcept
requires std::same_as<Storage, external>;

Parameters

Name Description

buffer

The buffer, 16-aligned, which outlives the parser and its document.

o

The limits and the revivers.

webcpp::document::basic_parser::~basic_parser

Destroys the parser and the document it was building.

Synopsis

~basic_parser() = default;

webcpp::document::basic_parser::operator=

Assignment operators

Synopses

A parser is not copied: its handler and its document refer to it.

basic_parser&
operator=(basic_parser const&) = delete;

A parser is not moved: its handler and its document refer to it.

webcpp::document::basic_parser::operator=

A parser is not copied: its handler and its document refer to it.

Synopsis

basic_parser&
operator=(basic_parser const&) = delete;

webcpp::document::basic_parser::operator=

A parser is not moved: its handler and its document refer to it.

Synopsis

basic_parser&
operator=(basic_parser&&) = delete;

webcpp::document::basic_parser::cause

The code of Boost.JSON behind an errc::syntax.

Synopsis

[[nodiscard]]
boost::system::error_code
cause() const noexcept;

Return Value

The code, or none.

Note

The return value should not be discarded.

webcpp::document::basic_parser::finish

Ends the text and returns the document, without the values on took.

Description

After it the parser takes no more text, which is errc::syntax, until reset.

Return Value

The document; the errors of write, and errc::syntax for text that ends early.

webcpp::document::basic_parser::observe

Delivers each value whose path matches selector once it completes, and keeps it.

Synopsis

template<class F>
result<void>
observe(
    std::string_view selector,
    F&& f)
requires detail::selector_callback<std::decay_t<F>, Traits, Storage>;

Return Value

Success; errc::type for a malformed selector.

Template Parameters

Name Description

F

A callable taking a read-only handle and a path_view, returning void or result<void>.

Parameters

Name Description

selector

The selector, as on takes it.

f

The callback.

See Also

"Selectors", in the guide.

webcpp::document::basic_parser::offset

How many bytes of the text were consumed.

Synopsis

[[nodiscard]]
uint64_t
offset() const noexcept;

Return Value

The count; after an errc::syntax, the offset where the error was found.

Note

The return value should not be discarded.

webcpp::document::basic_parser::on

Delivers each value whose path matches selector once it completes, then removes it from the document, its blocks zeroed and freed: a stream of a million elements needs the memory of one.

Synopsis

template<class F>
result<void>
on(
    std::string_view selector,
    F&& f)
requires detail::selector_callback<std::decay_t<F>, Traits, Storage>;

Description

The selector is $ then any sequence of .name, ['name'], [n], [] and .*. A scalar completes when its token ends, an array or object when it closes; a value inside another that also matches is delivered first.

Return Value

Success; errc::type for a malformed selector.

Template Parameters

Name Description

F

A callable taking a read-only handle and a path_view, returning void or result<void>.

Parameters

Name Description

selector

The selector.

f

The callback.

See Also

"Selectors", in the guide.

webcpp::document::basic_parser::reset

Makes the parser ready for another text, its selectors kept.

Synopsis

void
reset() noexcept;

webcpp::document::basic_parser::write

Feeds the next chunk of the text, delivering each value a selector matches as it completes.

Synopsis

result<void>
write(std::string_view chunk);

Return Value

Success; errc::syntax for malformed text, a second text after the first included, errc::range for a number the traits refuse, errc::capacity past a limit, or the error a callback returned.

Parameters

Name Description

chunk

The chunk: any split of the text, down to one byte.

webcpp::document::basic_stream_parser

The parser of a stream of documents: write takes the bytes in chunks of any size, down to one byte, and each document is delivered as soon as the bytes that end it are consumed.

Synopsis

template<class Traits = default_traits>
class basic_stream_parser;

Description

It is sans-IO: it reads no socket or file, so it serves any source, a socket read by hand, an actor’s messages, or JavaScript through Emscripten; <webcpp/document/asio.hpp> reads it from Boost.Asio’s streams. Each document is parsed as parse parses one text, with the same key order, numbers and limits, into a new owned document. The parser reads one document at a time: at a document’s end it moves it to a queue that next takes from, or hands it to the callback of the push form, and begins the next on the bytes that follow, in the same chunk.

It raises nothing of its own, since its input is the network’s: every failure is returned. An exception the callback of the push form throws propagates; the bytes up to the document it was given are consumed, which offset counts, and the parser takes the rest by another write.

Member Functions

Name Description

basic_stream_parser [constructor]

Constructors

~basic_stream_parser [destructor]

Destroys the parser, the document it was reading and those next has not taken.

operator=

Assignment operators

documents

How many documents were delivered, by next or to a callback.

error_cause

The code of Boost.JSON behind the error last delivered, when it is an errc::syntax that Boost.JSON’s parser found.

error_line

The line where the error last delivered was found: by next, to a callback, or returned by write or finish.

error_offset

The offset in the stream where the error last delivered was found.

finish

finish overloads

finished

Whether finish ended the stream, until reset.

line

The line of the last byte consumed, from 1; a n belongs to the line it ends.

next

Takes the oldest document the stream completed.

offset

How many bytes of the stream were consumed.

reset

Makes the parser ready for another stream: the documents not taken and any error dropped.

write

write overloads

Template Parameters

Name Description

Traits

The traits of the documents.

See Also

"Streams of documents", in the guide.

webcpp::document::basic_stream_parser::basic_stream_parser

Constructors

Synopses

A parser is not copied: its inner parser refers to itself.

A parser is not moved: its inner parser refers to itself.

A parser of a stream framed as o says.

explicit
basic_stream_parser(stream_options const& o = {});

Parameters

Name Description

o

The framing, the policy on errors, and the limits and revivers of each document.

webcpp::document::basic_stream_parser::basic_stream_parser

A parser is not copied: its inner parser refers to itself.

Synopsis

basic_stream_parser(basic_stream_parser const&) = delete;

webcpp::document::basic_stream_parser::basic_stream_parser

A parser is not moved: its inner parser refers to itself.

Synopsis

basic_stream_parser(basic_stream_parser&&) = delete;

webcpp::document::basic_stream_parser::basic_stream_parser

A parser of a stream framed as o says.

Synopsis

explicit
basic_stream_parser(stream_options const& o = {});

Description

on_error::skip with framing::concatenated is refused: every call of the parser returns boost::system::errc::invalid_argument.

Parameters

Name Description

o

The framing, the policy on errors, and the limits and revivers of each document.

webcpp::document::basic_stream_parser::~basic_stream_parser

Destroys the parser, the document it was reading and those next has not taken.

Synopsis

~basic_stream_parser() = default;

webcpp::document::basic_stream_parser::operator=

Assignment operators

Synopses

A parser is not copied: its inner parser refers to itself.

A parser is not moved: its inner parser refers to itself.

webcpp::document::basic_stream_parser::operator=

A parser is not copied: its inner parser refers to itself.

Synopsis

basic_stream_parser&
operator=(basic_stream_parser const&) = delete;

webcpp::document::basic_stream_parser::operator=

A parser is not moved: its inner parser refers to itself.

Synopsis

basic_stream_parser&
operator=(basic_stream_parser&&) = delete;

webcpp::document::basic_stream_parser::documents

How many documents were delivered, by next or to a callback.

Synopsis

[[nodiscard]]
uint64_t
documents() const noexcept;

Return Value

The count.

Note

The return value should not be discarded.

webcpp::document::basic_stream_parser::error_cause

The code of Boost.JSON behind the error last delivered, when it is an errc::syntax that Boost.JSON’s parser found.

Synopsis

[[nodiscard]]
boost::system::error_code
error_cause() const noexcept;

Return Value

The code, extra_data for a second document on a line or in a record; or none.

Note

The return value should not be discarded.

webcpp::document::basic_stream_parser::error_line

The line where the error last delivered was found: by next, to a callback, or returned by write or finish.

Synopsis

[[nodiscard]]
uint64_t
error_line() const noexcept;

Return Value

The line, from 1; 0 before any error.

Note

The return value should not be discarded.

webcpp::document::basic_stream_parser::error_offset

The offset in the stream where the error last delivered was found.

Synopsis

[[nodiscard]]
uint64_t
error_offset() const noexcept;

Return Value

The offset of the byte that the error was found at; 0 before any error.

Note

The return value should not be discarded.

webcpp::document::basic_stream_parser::finish

finish overloads

Synopses

Ends the stream, closing its last document, a top-level number or a last line without its n, and queueing it for next.

Ends the stream, handing the last document to a callback, after those next has not taken.

template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
finish(F&& on_document);

Return Value

  • How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream, as write returns it, and errc::syntax for a document that the stream’s end cuts short.

  • How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as the other finish returns it.

Template Parameters

Name Description

F

A callable taking a result<basic_document<Traits>>, returning void or result<void>.

Parameters

Name Description

on_document

The callback, as the push form of write takes it.

webcpp::document::basic_stream_parser::finish

Ends the stream, closing its last document, a top-level number or a last line without its n, and queueing it for next.

Synopsis

Description

After it the parser takes no more bytes, which is errc::syntax, until reset; an empty write, and another finish, complete nothing.

Return Value

How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream, as write returns it, and errc::syntax for a document that the stream’s end cuts short.

webcpp::document::basic_stream_parser::finish

Ends the stream, handing the last document to a callback, after those next has not taken.

Synopsis

template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
finish(F&& on_document);

Return Value

How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as the other finish returns it.

Template Parameters

Name Description

F

A callable taking a result<basic_document<Traits>>, returning void or result<void>.

Parameters

Name Description

on_document

The callback, as the push form of write takes it.

webcpp::document::basic_stream_parser::finished

Whether finish ended the stream, until reset.

Synopsis

[[nodiscard]]
bool
finished() const noexcept;

Description

Tip: async_read_document completes with eof without reading once it holds, for a socket reports its end to one read only on Linux’s epoll.

Return Value

true after finish.

Note

The return value should not be discarded.

webcpp::document::basic_stream_parser::line

The line of the last byte consumed, from 1; a n belongs to the line it ends.

Synopsis

[[nodiscard]]
uint64_t
line() const noexcept;

Return Value

The line; 1 before any byte.

Note

The return value should not be discarded.

webcpp::document::basic_stream_parser::next

Takes the oldest document the stream completed.

Return Value

The document; none when no document is complete; with on_error::skip, the error of a document dropped, in its place, which error_line names; with on_error::stop, once the documents before it are taken, the error that stopped the stream, every time.

webcpp::document::basic_stream_parser::offset

How many bytes of the stream were consumed.

Synopsis

[[nodiscard]]
uint64_t
offset() const noexcept;

Return Value

The count; after the stream stopped, the offset where its error was found.

Note

The return value should not be discarded.

webcpp::document::basic_stream_parser::reset

Makes the parser ready for another stream: the documents not taken and any error dropped.

Synopsis

void
reset() noexcept;

webcpp::document::basic_stream_parser::write

write overloads

Synopses

Consumes the next bytes of the stream, queueing each document they complete for next.

result<std::size_t>
write(std::string_view bytes);

Consumes the next bytes of the stream, handing each document they complete to a callback, in order, after those next has not taken.

template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
write(
    std::string_view bytes,
    F&& on_document);

Return Value

  • How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream: errc::syntax for malformed text, errc::range for a number the traits refuse, errc::capacity past a limit. The documents completed before it stay queued.

  • How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as write returns it.

Template Parameters

Name Description

F

A callable taking a result<basic_document<Traits>>, returning void or result<void>.

Parameters

Name Description

bytes

The bytes: any split of the stream, down to one byte.

on_document

The callback: given each document, and with on_error::skip the error of each document dropped, in the stream’s order. It may not call the parser’s write, finish or reset. A failure it returns stops the stream with that error.

webcpp::document::basic_stream_parser::write

Consumes the next bytes of the stream, queueing each document they complete for next.

Synopsis

result<std::size_t>
write(std::string_view bytes);

Return Value

How many documents, and dropped documents’ errors, it queued; or the error that stopped the stream: errc::syntax for malformed text, errc::range for a number the traits refuse, errc::capacity past a limit. The documents completed before it stay queued.

Parameters

Name Description

bytes

The bytes: any split of the stream, down to one byte.

webcpp::document::basic_stream_parser::write

Consumes the next bytes of the stream, handing each document they complete to a callback, in order, after those next has not taken.

Synopsis

template<class F>
requires detail::stream_callback<F, Traits>
result<std::size_t>
write(
    std::string_view bytes,
    F&& on_document);

Return Value

How many documents, and dropped documents’ errors, it handed over; or the error that stopped the stream, as write returns it.

Template Parameters

Name Description

F

A callable taking a result<basic_document<Traits>>, returning void or result<void>.

Parameters

Name Description

bytes

The bytes: any split of the stream, down to one byte.

on_document

The callback: given each document, and with on_error::skip the error of each document dropped, in the stream’s order. It may not call the parser’s write, finish or reset. A failure it returns stops the stream with that error.

webcpp::document::basic_value

A handle that reads and writes: one place in one document, as basic_const_value, with JavaScript’s assignment, delete, Object.assign and the array methods.

Synopsis

template<
    class Traits = default_traits,
    class Storage = owned>
class basic_value
    : public basic_const_value<Traits, Storage>

Description

Every writing function returns result and either completes or changes nothing, a full heap included. Assignment to a handle writes to its place, with exceptions only: it never rebinds the handle.

So a handle is not a regular value, and must not be swapped or sorted: std::swap(x, y) assigns through both, leaving both places holding y’s value, and std::sort over handles writes the document’s values the same way.

Base Classes

Name Description

basic_const_value<Traits, Storage>

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

Member Functions

Name Description

basic_value [constructor]

Constructors

~basic_value [destructor]

Nothing to free: the document owns the value.

operator=

Assignment operators

as_array

The array as a vector-like container that reads and writes: push_back, insert, erase, resize, and iterators that <algorithm> and <ranges> take.

as_object

The object as a sorted-map-like container that reads and writes: find, insert, erase, bounds by key, and iterators that <algorithm> and <ranges> take.

assign_from

Object.assign(v, source): sets each member of source here, in its order, as one operation.

at

Array.prototype.at: the element index, a negative index counting from the end.

erase

delete v[key]: erases an object’s member and its subtree.

operator[]

Subscript operators

pop

Array.prototype.pop: removes the last element.

push

Array.prototype.push: appends each value.

reverse

Array.prototype.reverse: reverses the elements in place.

set

Stores a value at this place, v = x: a missing member is created, an element past the end fills the elements between with null, and an existing value is replaced.

set_length

a.length = n: truncates the array, or appends null up to n.

shift

Array.prototype.shift: removes the first element.

splice

splice overloads

unshift

Array.prototype.unshift: inserts each value at the front, in order.

Using Declarations

Name Description

as_array

The read-only view of an array, for a const handle.

as_object

The read-only view of an object, for a const handle.

at

The read-only at of a read-only handle, for a const one.

operator[]

The read-only navigation of a read-only handle, for a const one.

Friends

Name Description

webcpp::document::basic_const_value

A read-only handle: one place in one document, cheap to copy, which reads what is there now.

webcpp::document::basic_document

A JSON document: one value, in one relocatable image.

Non-Member Functions

Name Description

operator==

Whether two values are equal, structurally: the same kind; numbers by value (1, 1.0 and 1e0 are equal); strings, dates and bytes by value; arrays element by element; objects with the same keys, each with an equal value, in any order.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

See Also

"Handles", in the guide.

"Arrays", in the guide.

webcpp::document::basic_value::basic_value

Constructors

Synopses

A handle that names nothing.

basic_value() noexcept = default;

A handle of the same place.

basic_value(basic_value const& other) = default;

A handle of the same place.

basic_value(basic_value&& other) noexcept = default;

Parameters

Name Description

other

The handle.

webcpp::document::basic_value::basic_value

A handle that names nothing.

Synopsis

basic_value() noexcept = default;

webcpp::document::basic_value::basic_value

A handle of the same place.

Synopsis

basic_value(basic_value const& other) = default;

Parameters

Name Description

other

The handle.

webcpp::document::basic_value::basic_value

A handle of the same place.

Synopsis

basic_value(basic_value&& other) noexcept = default;

Parameters

Name Description

other

The handle.

webcpp::document::basic_value::~basic_value

Nothing to free: the document owns the value.

Synopsis

~basic_value() = default;

webcpp::document::basic_value::operator=

Assignment operators

Synopses

Stores a deep copy of another handle’s value at this place, as set does, throwing on failure; only with exceptions.

basic_value<Traits, Storage>&
operator=(basic_value const& v)
requires detail::exceptions_enabled<Traits>;

Stores a value at this place, v = x, as set does, throwing on failure; only with exceptions.

template<class T>
basic_value<Traits, Storage>&
operator=(T const& v)
requires detail::exceptions_enabled<T> &&
             (detail::settable<Traits, T> || std::same_as<T, undefined_t>);

Exceptions

Name Thrown on

type_error

For a place that is nothing.

capacity_error

When there is no room.

Return Value

This handle.

Template Parameters

Name Description

T

The value’s type.

Parameters

Name Description

v

The handle.

See Also

"The sugar that throws", in the guide.

webcpp::document::basic_value::operator=

Stores a deep copy of another handle’s value at this place, as set does, throwing on failure; only with exceptions.

Synopsis

basic_value<Traits, Storage>&
operator=(basic_value const& v)
requires detail::exceptions_enabled<Traits>;

Description

It never rebinds the handle: assignment to a handle writes to its place.

Exceptions

Name Thrown on

type_error

For a place that is nothing.

capacity_error

When there is no room.

Return Value

This handle.

Parameters

Name Description

v

The handle.

webcpp::document::basic_value::operator=

Stores a value at this place, v = x, as set does, throwing on failure; only with exceptions.

Synopsis

template<class T>
basic_value<Traits, Storage>&
operator=(T const& v)
requires detail::exceptions_enabled<T> &&
             (detail::settable<Traits, T> || std::same_as<T, undefined_t>);

Exceptions

Name Thrown on

type_error

For a place that is nothing.

capacity_error

When there is no room.

Return Value

This handle.

Template Parameters

Name Description

T

The value’s type.

Parameters

Name Description

v

The value.

See Also

"The sugar that throws", in the guide.

webcpp::document::basic_value::as_array

The array as a vector-like container that reads and writes: push_back, insert, erase, resize, and iterators that <algorithm> and <ranges> take.

Synopsis

[[nodiscard]]
result<basic_array_ref<basic_value<Traits, Storage>>>
as_array() noexcept;

Return Value

The view, defined in containers.hpp; errc::type for any other kind.

Note

The return value should not be discarded.

See Also

"Arrays", in the guide.

webcpp::document::basic_value::as_object

The object as a sorted-map-like container that reads and writes: find, insert, erase, bounds by key, and iterators that <algorithm> and <ranges> take.

Synopsis

[[nodiscard]]
result<basic_object_ref<basic_value<Traits, Storage>>>
as_object() noexcept;

Return Value

The view, defined in containers.hpp; errc::type for any other kind.

Note

The return value should not be discarded.

See Also

"Key order", in the guide.

webcpp::document::basic_value::assign_from

Object.assign(v, source): sets each member of source here, in its order, as one operation.

Synopsis

template<class S>
[[nodiscard]]
result<void>
assign_from(basic_const_value<Traits, S> const& source) noexcept;

Description

An array’s elements are its members "0" to "n-1", and null or undefined adds nothing.

Return Value

Success; errc::type when this is not an object or source is a scalar, errc::capacity when there is no room.

Note

The return value should not be discarded.

Template Parameters

Name Description

S

The source’s storage.

Parameters

Name Description

source

The source.

webcpp::document::basic_value::at

Array.prototype.at: the element index, a negative index counting from the end.

Synopsis

basic_value
at(int64_t index) noexcept;

Return Value

Its handle; nothing out of range or for any other kind.

Parameters

Name Description

index

The index.

webcpp::document::basic_value::erase

delete v[key]: erases an object’s member and its subtree.

Synopsis

[[nodiscard]]
result<bool>
erase(std::string_view key) noexcept;

Return Value

Whether it was there; errc::type for any other kind than an object.

Note

The return value should not be discarded.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_value::operator[]

Subscript operators

Synopses

The member key, v.key, which never fails.

basic_value
operator[](std::string_view key) noexcept;

The element index, v[index], which never fails.

basic_value
operator[](uint64_t index) noexcept;

Return Value

  • Its handle: the member, a missing member of an object, or nothing.

  • Its handle: the element, a missing element of an array, or nothing.

Parameters

Name Description

key

The key, in UTF-8.

index

The index.

webcpp::document::basic_value::operator[]

The member key, v.key, which never fails.

Synopsis

basic_value
operator[](std::string_view key) noexcept;

Return Value

Its handle: the member, a missing member of an object, or nothing.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::basic_value::operator[]

The element index, v[index], which never fails.

Synopsis

basic_value
operator[](uint64_t index) noexcept;

Return Value

Its handle: the element, a missing element of an array, or nothing.

Parameters

Name Description

index

The index.

webcpp::document::basic_value::pop

Array.prototype.pop: removes the last element.

Synopsis

[[nodiscard]]
result<basic_document<Traits, owned>>
pop() noexcept;

Return Value

A new owned document holding it, or holding undefined when the array is empty; errc::type for any other kind.

Note

The return value should not be discarded.

webcpp::document::basic_value::push

Array.prototype.push: appends each value.

Synopsis

template<class... T>
[[nodiscard]]
result<uint64_t>
push(T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);

Return Value

The new length; errc::type for any other kind than an array, errc::capacity when there is no room, and the errors of set for a value.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The values’ types, each settable.

Parameters

Name Description

v

The values.

webcpp::document::basic_value::reverse

Array.prototype.reverse: reverses the elements in place.

Synopsis

[[nodiscard]]
result<void>
reverse() noexcept;

Return Value

Success; errc::type for any other kind.

Note

The return value should not be discarded.

webcpp::document::basic_value::set

Stores a value at this place, v = x: a missing member is created, an element past the end fills the elements between with null, and an existing value is replaced.

Synopsis

template<class T>
[[nodiscard]]
result<void>
set(T const& v) noexcept
requires detail::settable<Traits, T> || std::same_as<T, undefined_t>;

Description

Settable: bool; any integer type; the decimal types (finite, else errc::range); the traits’ number; std::string_view, const char*, std::string (valid UTF-8, else errc::type), std::u16string_view; std::span<const std::byte> (bytes); a date_ms (within JavaScript’s range, else errc::range); nullptr or null; the literals object and array; a handle or a document of the same traits, copied deeply; and undefined, which erases a member, stores null in an element and makes a root hold undefined. Numbers are stored by value.

Return Value

Success; errc::type for a place that is nothing, errc::capacity when there is no room, and the errors above.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The value’s type.

Parameters

Name Description

v

The value.

webcpp::document::basic_value::set_length

a.length = n: truncates the array, or appends null up to n.

Synopsis

[[nodiscard]]
result<void>
set_length(uint64_t n) noexcept;

Return Value

Success; errc::type for any other kind.

Note

The return value should not be discarded.

Parameters

Name Description

n

The new length.

webcpp::document::basic_value::shift

Array.prototype.shift: removes the first element.

Synopsis

[[nodiscard]]
result<basic_document<Traits, owned>>
shift() noexcept;

Return Value

A new owned document holding it, or holding undefined when the array is empty; errc::type for any other kind.

Note

The return value should not be discarded.

webcpp::document::basic_value::splice

splice overloads

Synopses

Array.prototype.splice(start): removes every element from start.

[[nodiscard]]
result<basic_document<Traits, owned>>
splice(int64_t start) noexcept;

Array.prototype.splice: removes count elements from start and inserts each value there.

template<class... T>
[[nodiscard]]
result<basic_document<Traits, owned>>
splice(
    int64_t start,
    int64_t count,
    T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);

Return Value

A new owned document holding the array of the removed elements; errc::type for any other kind.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The values’ types, each settable.

Parameters

Name Description

start

The first index, a negative one counting from the end.

count

How many to remove.

v

The values to insert.

See Also

"Arrays", in the guide.

webcpp::document::basic_value::splice

Array.prototype.splice(start): removes every element from start.

Synopsis

[[nodiscard]]
result<basic_document<Traits, owned>>
splice(int64_t start) noexcept;

Return Value

A new owned document holding the array of the removed elements; errc::type for any other kind.

Note

The return value should not be discarded.

Parameters

Name Description

start

The first index, a negative one counting from the end.

webcpp::document::basic_value::splice

Array.prototype.splice: removes count elements from start and inserts each value there.

Synopsis

template<class... T>
[[nodiscard]]
result<basic_document<Traits, owned>>
splice(
    int64_t start,
    int64_t count,
    T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);

Description

A negative start counts from the end, clamped at 0, and one past the length is the length; a negative count is 0, and one past the end removes to the end.

Return Value

A new owned document holding the array of the removed elements; errc::type for any other kind.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The values’ types, each settable.

Parameters

Name Description

start

The first index.

count

How many to remove.

v

The values to insert.

See Also

"Arrays", in the guide.

webcpp::document::basic_value::unshift

Array.prototype.unshift: inserts each value at the front, in order.

Synopsis

template<class... T>
[[nodiscard]]
result<uint64_t>
unshift(T const&... v) noexcept
requires (detail::settable<Traits, T> && ...);

Return Value

The new length; errc::type for any other kind than an array, errc::capacity when there is no room, and the errors of set for a value.

Note

The return value should not be discarded.

Template Parameters

Name Description

T

The values’ types, each settable.

Parameters

Name Description

v

The values.

webcpp::document::basic_value::as_array

The read-only view of an array, for a const handle.

Synopsis

using basic_const_value<Traits, Storage>::as_array;

webcpp::document::basic_value::as_object

The read-only view of an object, for a const handle.

Synopsis

using basic_const_value<Traits, Storage>::as_object;

webcpp::document::basic_value::at

The read-only at of a read-only handle, for a const one.

Synopsis

using basic_const_value<Traits, Storage>::at;

webcpp::document::basic_value::operator[]

The read-only navigation of a read-only handle, for a const one.

Synopsis

using basic_const_value<Traits, Storage>::operator[];

webcpp::document::capacity_error

No room left: a full external buffer, a document at its limit, a parser’s limit.

Synopsis

class capacity_error
    : public error

Base Classes

Name Description

error

What every exception of document derives from: a std::runtime_error holding its code.

Using Declarations

Name Description

capacity_error

Constructs the exception of a code, as error does.

webcpp::document::capacity_error::capacity_error

Constructs the exception of a code, as error does.

Synopsis

using error::capacity_error;

Introduced Symbols

Name Description

error [constructor]

Constructs the exception of code, whose message is the code’s.

webcpp::document::decimal_digits

A finite decimal number as JavaScript’s Number::toString sees it: its value is 0.d1 d2 ... dk times ten to the power point, with k the count of its significant digits.

Synopsis

struct decimal_digits;

Description

Zero has no digits: count is 0, point 0 and negative false. Any other value has between 1 and 34 digits, the first and the last of them not '0', so that every number has exactly one decimal_digits: 1.5, 1.50 and 15e-1 are all {false, "15", 2, 1}.

Data Members

Name Description

count

How many significant digits there are, k; 0 for zero.

digits

The significant digits, '0' to '9', in the first count places.

negative

Whether the number is below zero; never true for zero.

point

Where the decimal point is, n: after this many digits, which may be negative.

webcpp::document::decimal_digits::count

How many significant digits there are, k; 0 for zero.

Synopsis

uint32_t count = 0;

webcpp::document::decimal_digits::digits

The significant digits, '0' to '9', in the first count places.

Synopsis

std::array<char, 34> digits = {};

webcpp::document::decimal_digits::negative

Whether the number is below zero; never true for zero.

Synopsis

bool negative = false;

webcpp::document::decimal_digits::point

Where the decimal point is, n: after this many digits, which may be negative.

Synopsis

int32_t point = 0;

webcpp::document::default_traits

The default traits: exact numbers, UTF-8 strings, base64 bytes and JavaScript’s key order.

Synopsis

struct default_traits;

Description

A number is stored by value: an integer that fits integer as one, any other as number.

Type Aliases

Name Description

array

The behaviour of arrays.

boolean

The type of true and false.

bytes

How stringify writes bytes.

date

The type of a date.

integer

The type of a number that is an integer and fits.

number

The type of any other number: exact, 34 significant digits.

object

The order of an object’s keys.

string

The encoding strings and keys are stored in.

Derived Classes

Name Description

js_traits

The traits of JavaScript itself: doubles, UTF-16 strings, and bytes written as a Uint8Array.

See Also

"Traits", in the guide.

webcpp::document::default_traits::array

The behaviour of arrays.

Synopsis

using array = dense;

webcpp::document::default_traits::boolean

The type of true and false.

Synopsis

using boolean = bool;

webcpp::document::default_traits::bytes

How stringify writes bytes.

Synopsis

using bytes = base64;

webcpp::document::default_traits::date

The type of a date.

Synopsis

using date = date_ms;

webcpp::document::default_traits::integer

The type of a number that is an integer and fits.

Synopsis

using integer = int64_t;

webcpp::document::default_traits::number

The type of any other number: exact, 34 significant digits.

Synopsis

using number = boost::decimal::decimal128_t;

webcpp::document::default_traits::object

The order of an object’s keys.

Synopsis

using object = javascript_order;

webcpp::document::default_traits::string

The encoding strings and keys are stored in.

Synopsis

using string = utf8;

webcpp::document::dense

The array policy: no holes, and writing past the end fills with null.

Synopsis

struct dense;

Static Data Members

Name Description

name

The policy’s name in the image’s layout tag.

webcpp::document::dense::name

The policy’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "dense";

webcpp::document::error

What every exception of document derives from: a std::runtime_error holding its code.

Synopsis

class error
    : public std::runtime_error

Base Classes

Name Description

std::runtime_error

One of two subclasses of exception.

Member Functions

Name Description

error [constructor]

Constructors

code

The code of the failure.

Derived Classes

Name Description

capacity_error

No room left: a full external buffer, a document at its limit, a parser’s limit.

image_error

Bytes that are not a document’s image, an image of another format, or a damaged one.

range_error

JavaScript’s RangeError: a number that does not fit, or an index out of range.

syntax_error

JavaScript’s SyntaxError: malformed JSON text, at a byte offset.

type_error

JavaScript’s TypeError: a value of another kind, or a write through nothing.

See Also

"The sugar that throws", in the guide.

webcpp::document::error::error

Constructors

Synopses

What every exception of document derives from: a `std::runtime_error` holding its code.

error(error const&) = default;

What every exception of document derives from: a `std::runtime_error` holding its code.

error(error&&) = default;

Constructs the exception of code, whose message is the code’s.

explicit
error(boost::system::error_code code);

Parameters

Name Description

code

The code of the failure.

See Also

"The sugar that throws", in the guide.

webcpp::document::error::error

Constructs the exception of code, whose message is the code’s.

Synopsis

explicit
error(boost::system::error_code code);

Parameters

Name Description

code

The code of the failure.

webcpp::document::error::code

The code of the failure.

Synopsis

[[nodiscard]]
boost::system::error_code
code() const noexcept;

Return Value

The code given at construction.

Note

The return value should not be discarded.

webcpp::document::external

The storage of a document whose image is a buffer its user owns, such as a browser’s memory: when it is full, an operation returns errc::capacity and changes nothing.

Synopsis

struct external;

See Also

"Images", in the guide.

webcpp::document::image_error

Bytes that are not a document’s image, an image of another format, or a damaged one.

Synopsis

class image_error
    : public error

Base Classes

Name Description

error

What every exception of document derives from: a std::runtime_error holding its code.

Using Declarations

Name Description

image_error

Constructs the exception of a code, as error does.

webcpp::document::image_error::image_error

Constructs the exception of a code, as error does.

Synopsis

using error::image_error;

Introduced Symbols

Name Description

error [constructor]

Constructs the exception of code, whose message is the code’s.

webcpp::document::image_header

The 64 bytes at the start of every image, in the byte order of the machine that wrote it.

Synopsis

struct image_header;

Description

The heap follows it, at offset 64.

Data Members

Name Description

byte_order

byte_order_mark, as the writer’s machine writes it.

container

The offset of the tree’s container from the image’s start.

layout

A hash of the node’s, the container’s and the heap’s sizes and alignments, and of the traits.

magic

image_magic.

next_id

The id the next node created takes; ids are never reused.

reserved

Zero.

reserved_4

Zero, and kept for a future version: an image whose reserved bytes are not zero is refused.

seal

Zero for an image that is not sealed; else the image’s CRC-32C, with these eight bytes taken as zero, in the low 32 bits, and the high bit set, which marks the image sealed.

size

The size of the whole image, header included.

version

format_version.

See Also

"Images", in the guide.

webcpp::document::image_header::byte_order

byte_order_mark, as the writer’s machine writes it.

Synopsis

uint32_t byte_order;

webcpp::document::image_header::container

The offset of the tree’s container from the image’s start.

Synopsis

uint64_t container;

webcpp::document::image_header::layout

A hash of the node’s, the container’s and the heap’s sizes and alignments, and of the traits.

Synopsis

uint32_t layout;

webcpp::document::image_header::magic

Synopsis

std::array<char, 8> magic;

webcpp::document::image_header::next_id

The id the next node created takes; ids are never reused.

Synopsis

uint64_t next_id;

webcpp::document::image_header::reserved

Zero.

Synopsis

std::array<std::byte, 8> reserved;

webcpp::document::image_header::reserved_4

Zero, and kept for a future version: an image whose reserved bytes are not zero is refused.

Synopsis

uint32_t reserved_4;

webcpp::document::image_header::seal

Zero for an image that is not sealed; else the image’s CRC-32C, with these eight bytes taken as zero, in the low 32 bits, and the high bit set, which marks the image sealed.

Synopsis

uint64_t seal;

webcpp::document::image_header::size

The size of the whole image, header included.

Synopsis

uint64_t size;

webcpp::document::image_header::version

Synopsis

uint32_t version;

webcpp::document::image_pool

Fixed blocks for images, 16-aligned and zeroed, allocated once: block_bytes times blocks.

Synopsis

class image_pool;

Description

A block is leased by acquire, or by a basic_image_reader for each image it reads, and goes back when its lease does. When every block is leased, the pool says errc::capacity, the backpressure of a stream of images, whose reader takes no more bytes until a block is given back. The pool must outlive its leases and its readers; moving it moves neither its blocks nor its leases.

Note

A pool is not thread-safe: a block is leased and given back, a lease destroyed included, on one thread at a time.

Member Functions

Name Description

image_pool [constructor]

Constructors

~image_pool [destructor]

Frees the blocks, every one of which must have been given back.

operator=

Assignment operators

acquire

Leases a free block, whose bytes are all zero.

available

How many blocks are free.

block_bytes

The size of each block.

blocks

How many blocks the pool has.

Static Member Functions

Name Description

create

Makes a pool of blocks blocks of block_bytes bytes each, in one allocation.

Friends

Name Description

webcpp::document::basic_image_reader

Reads a stream of images, one after another with nothing between them, into the blocks of an image_pool, and opens each in place as a leased_document.

See Also

"Images", in the guide.

webcpp::document::image_pool::image_pool

Constructors

Synopses

A pool is not copied.

image_pool(image_pool const&) = delete;

Takes the blocks of other, which holds none after it; the leases of its blocks stay valid.

image_pool(image_pool&& other) noexcept;

Parameters

Name Description

other

The pool.

webcpp::document::image_pool::image_pool

A pool is not copied.

Synopsis

image_pool(image_pool const&) = delete;

webcpp::document::image_pool::image_pool

Takes the blocks of other, which holds none after it; the leases of its blocks stay valid.

Synopsis

image_pool(image_pool&& other) noexcept;

Parameters

Name Description

other

The pool.

webcpp::document::image_pool::~image_pool

Frees the blocks, every one of which must have been given back.

Synopsis

~image_pool();

webcpp::document::image_pool::operator=

Assignment operators

Synopses

A pool is not copied.

image_pool&
operator=(image_pool const&) = delete;

Frees this pool’s blocks, then takes the blocks of other, which holds none after it.

image_pool&
operator=(image_pool&& other) noexcept;

Return Value

This pool.

Parameters

Name Description

other

The pool.

webcpp::document::image_pool::operator=

A pool is not copied.

Synopsis

image_pool&
operator=(image_pool const&) = delete;

webcpp::document::image_pool::operator=

Frees this pool’s blocks, then takes the blocks of other, which holds none after it.

Synopsis

image_pool&
operator=(image_pool&& other) noexcept;

Return Value

This pool.

Parameters

Name Description

other

The pool.

webcpp::document::image_pool::acquire

Leases a free block, whose bytes are all zero.

Synopsis

result<pool_block>
acquire() noexcept;

Return Value

The lease; errc::capacity when every block is leased, or for a moved-from pool.

webcpp::document::image_pool::available

How many blocks are free.

Synopsis

[[nodiscard]]
std::size_t
available() const noexcept;

Return Value

The count, 0 for a moved-from pool.

Note

The return value should not be discarded.

webcpp::document::image_pool::block_bytes

The size of each block.

Synopsis

[[nodiscard]]
std::size_t
block_bytes() const noexcept;

Return Value

The size, 0 for a moved-from pool.

Note

The return value should not be discarded.

webcpp::document::image_pool::blocks

How many blocks the pool has.

Synopsis

[[nodiscard]]
std::size_t
blocks() const noexcept;

Return Value

The count, 0 for a moved-from pool.

Note

The return value should not be discarded.

webcpp::document::image_pool::create

Makes a pool of blocks blocks of block_bytes bytes each, in one allocation.

Synopsis

static
result<image_pool>
create(
    std::size_t block_bytes,
    std::size_t blocks) noexcept;

Return Value

The pool; errc::range for a size or a count outside those bounds, and errc::capacity when the allocation fails.

Parameters

Name Description

block_bytes

The size of each block, a multiple of 16 and at least 64, the size of an image’s header; the largest image the pool holds.

blocks

How many blocks, at least one.

webcpp::document::insertion_order

The object policy that keeps every key in the order it was created, whatever the key.

Synopsis

struct insertion_order;

Static Data Members

Name Description

name

The policy’s name in the image’s layout tag.

webcpp::document::insertion_order::name

The policy’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "insertion";

webcpp::document::javascript_bytes

The bytes policy that writes bytes as JSON.stringify writes a Uint8Array: {"0":1,"1":2}.

Synopsis

struct javascript_bytes;

Static Data Members

Name Description

name

The policy’s name in the image’s layout tag.

webcpp::document::javascript_bytes::name

The policy’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "javascript";

webcpp::document::javascript_order

The object policy of JavaScript: array-index keys ascending, then the others as created.

Synopsis

struct javascript_order;

Static Data Members

Name Description

name

The policy’s name in the image’s layout tag.

See Also

"Key order", in the guide.

webcpp::document::javascript_order::name

The policy’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "javascript";

webcpp::document::js_traits

The traits of JavaScript itself: doubles, UTF-16 strings, and bytes written as a Uint8Array.

Synopsis

struct js_traits
    : default_traits

Base Classes

Name Description

default_traits

The default traits: exact numbers, UTF-8 strings, base64 bytes and JavaScript’s key order.

Type Aliases

Name Description

bytes

How stringify writes bytes: as JSON.stringify writes a Uint8Array.

number

The type of a number that is not an integer that fits: JavaScript’s own double.

string

The encoding strings and keys are stored in: UTF-16 code units, as JavaScript holds them.

See Also

"Traits", in the guide.

webcpp::document::js_traits::bytes

How stringify writes bytes: as JSON.stringify writes a Uint8Array.

Synopsis

using bytes = javascript_bytes;

webcpp::document::js_traits::number

The type of a number that is not an integer that fits: JavaScript’s own double.

Synopsis

using number = double;

webcpp::document::js_traits::string

The encoding strings and keys are stored in: UTF-16 code units, as JavaScript holds them.

Synopsis

using string = utf16;

webcpp::document::leased_document

A document opened in place in a block of an image_pool, holding the block’s lease: when the document goes, the block goes back to the pool, its bytes zeroed.

Synopsis

template<class Traits = default_traits>
class leased_document;

Description

The document is external: it reads and writes as any document does, within its image, and a change that needs more room than the image has returns errc::capacity. A leased document is moved, never copied; moving it invalidates its handles, and a moved-from one holds no document.

Member Functions

Name Description

leased_document [constructor]

Constructors

~leased_document [destructor]

Gives the block back, its bytes zeroed.

operator=

Assignment operators

empty

Whether the lease holds no document: an empty lease, or one moved from.

get

get overloads

image

The document’s image, at the start of its block.

operator[]

Subscript operators

root

root overloads

seal

Seals the image, as the document’s own seal does: the next change clears it.

sealed

Whether the image carries a seal.

Static Member Functions

Name Description

format

Formats a leased block as a new, empty document, holding null, which may grow to the whole block.

open

Opens the image in a leased block, in place.

Friends

Name Description

webcpp::document::basic_image_reader

Reads a stream of images, one after another with nothing between them, into the blocks of an image_pool, and opens each in place as a leased_document.

Template Parameters

Name Description

Traits

The traits the image was written with.

See Also

"Images", in the guide.

webcpp::document::leased_document::leased_document

Constructors

Synopses

The empty lease: no block and no document, the state a moved-from leased document is left in.

leased_document() noexcept;

A leased document is not copied: its block has one holder.

leased_document(leased_document const&) = delete;

Takes the document and the lease of other, which holds neither after it.

leased_document(leased_document&& other) noexcept = default;

Parameters

Name Description

other

The leased document.

webcpp::document::leased_document::leased_document

The empty lease: no block and no document, the state a moved-from leased document is left in.

Synopsis

leased_document() noexcept;

Description

An operation that fails completes with it, as a leased document of an error holds nothing.

webcpp::document::leased_document::leased_document

A leased document is not copied: its block has one holder.

Synopsis

leased_document(leased_document const&) = delete;

webcpp::document::leased_document::leased_document

Takes the document and the lease of other, which holds neither after it.

Synopsis

leased_document(leased_document&& other) noexcept = default;

Parameters

Name Description

other

The leased document.

webcpp::document::leased_document::~leased_document

Gives the block back, its bytes zeroed.

Synopsis

~leased_document() = default;

webcpp::document::leased_document::operator=

Assignment operators

Synopses

A leased document is not copied: its block has one holder.

Gives this document’s block back, then takes the document and the lease of other, which holds neither after it.

Return Value

This leased document.

Parameters

Name Description

other

The leased document.

webcpp::document::leased_document::operator=

A leased document is not copied: its block has one holder.

Synopsis

leased_document&
operator=(leased_document const&) = delete;

webcpp::document::leased_document::operator=

Gives this document’s block back, then takes the document and the lease of other, which holds neither after it.

Synopsis

leased_document&
operator=(leased_document&& other) noexcept;

Return Value

This leased document.

Parameters

Name Description

other

The leased document.

webcpp::document::leased_document::empty

Whether the lease holds no document: an empty lease, or one moved from.

Synopsis

[[nodiscard]]
bool
empty() const noexcept;

Return Value

Whether it holds no block; its document then reads as nothing.

Note

The return value should not be discarded.

webcpp::document::leased_document::get

get overloads

Synopses

The document.

The document, read only.

[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;

Return Value

The document, valid while this leased document is neither moved nor destroyed.

webcpp::document::leased_document::get

The document.

Synopsis

Return Value

The document, valid while this leased document is neither moved nor destroyed.

webcpp::document::leased_document::get

The document, read only.

Synopsis

[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;

Return Value

The document, valid while this leased document is neither moved nor destroyed.

Note

The return value should not be discarded.

webcpp::document::leased_document::image

The document’s image, at the start of its block.

Synopsis

[[nodiscard]]
std::span<std::byte const>
image() const noexcept;

Return Value

Its bytes, empty for a moved-from leased document.

Note

The return value should not be discarded.

webcpp::document::leased_document::operator[]

Subscript operators

Synopses

The member key of the document’s value, as root()[key].

basic_value<Traits, external>
operator[](std::string_view key) noexcept;

The member key of the document’s value, read only.

basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;

The element index of the document’s value, as root()[index].

basic_value<Traits, external>
operator[](uint64_t index) noexcept;

The element index of the document’s value, read only.

basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

index

The index.

webcpp::document::leased_document::operator[]

The member key of the document’s value, as root()[key].

Synopsis

basic_value<Traits, external>
operator[](std::string_view key) noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::leased_document::operator[]

The member key of the document’s value, read only.

Synopsis

basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::leased_document::operator[]

The element index of the document’s value, as root()[index].

Synopsis

basic_value<Traits, external>
operator[](uint64_t index) noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

index

The index.

webcpp::document::leased_document::operator[]

The element index of the document’s value, read only.

Synopsis

basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

index

The index.

webcpp::document::leased_document::root

root overloads

Synopses

The handle of the document’s value.

The handle of the document’s value, read only.

[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;

Return Value

  • The handle, which reads and writes.

  • The handle.

webcpp::document::leased_document::root

The handle of the document’s value.

Synopsis

Return Value

The handle, which reads and writes.

webcpp::document::leased_document::root

The handle of the document’s value, read only.

Synopsis

[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;

Return Value

The handle.

Note

The return value should not be discarded.

webcpp::document::leased_document::seal

Seals the image, as the document’s own seal does: the next change clears it.

Synopsis

result<void>
seal() noexcept;

Return Value

Success; errc::type for a moved-from leased document.

webcpp::document::leased_document::sealed

Whether the image carries a seal.

Synopsis

[[nodiscard]]
bool
sealed() const noexcept;

Return Value

Whether it is sealed; false for a moved-from leased document.

Note

The return value should not be discarded.

webcpp::document::leased_document::format

Formats a leased block as a new, empty document, holding null, which may grow to the whole block.

Synopsis

static
result<leased_document>
format(pool_block block) noexcept;

Return Value

The document; the errors of the function format of an external buffer, the block then given back.

Parameters

Name Description

block

The lease.

webcpp::document::leased_document::open

Opens the image in a leased block, in place.

Synopsis

static
result<leased_document>
open(pool_block block) noexcept;

Return Value

The document; the errors of the function open of an external buffer, the block then given back.

Parameters

Name Description

block

The lease, whose block holds the image from its first byte.

webcpp::document::literal

One value of a literal, converted implicitly from the C++ value it is written as.

Synopsis

class literal;

Description

It holds bool, any integer (character types aside), the decimal types, a binary64 floating-point value (stored only by traits whose number is binary, such as js_traits), text in UTF-8 or UTF-16, a date_ms, bytes, nullptr or null, and an object or an array. Text and bytes are copied, so a literal owns what it holds.

Member Functions

Name Description

literal [constructor]

Constructors

visit

Calls f with the value this literal holds, as one of: null_t, bool, std::int64_t, std::uint64_t, const decimal_digits& (a finite decimal), detail::non_finite_decimal, detail::binary64, std::string_view, std::u16string_view, date_ms, std::span<const std::byte>, const object& or const array&.

webcpp::document::literal::literal

Constructors

Synopses

An array.

A date.

literal(date_ms v) noexcept;

JavaScript’s null.

literal(null_t v) noexcept;

JavaScript’s null.

literal(std::nullptr_t v) noexcept;

An object.

Text in UTF-8, copied.

literal(std::string_view v);

Text in UTF-16, copied.

literal(std::u16string_view v);

Bytes, copied.

literal(std::span<std::byte const> v);

Text in UTF-8, copied.

literal(std::string const& v);

Text in UTF-8, copied; a null pointer holds null.

literal(char const* v);

Text in UTF-16, copied; a null pointer holds null.

literal(char16_t const* v);

A boolean, constrained so that a pointer does not convert to it.

template<std::same_as<std::vector<std::byte>> B>
literal(B v);

A binary64 floating-point value, which only traits whose number is binary store.

template<std::floating_point F>
requires (sizeof(F) == 8)
literal(F v) noexcept;

An integer, of any width and signedness, character types aside.

template<std::integral I>
requires (!std::same_as<I, bool> && !std::same_as<I, char> && !std::same_as<I, char8_t> &&
                 !std::same_as<I, char16_t> && !std::same_as<I, char32_t> &&
                 !std::same_as<I, wchar_t>)
literal(I v) noexcept;

Template Parameters

Name Description

B

bool.

D

The decimal type.

S

std::u16string, a template parameter, so that it is chosen only for a std::u16string itself.

V

std::vector<std::byte>.

F

The floating-point type, of 8 bytes.

I

The integer type.

Parameters

Name Description

v

The array.

webcpp::document::literal::literal

An array.

Synopsis

literal(array v);

Parameters

Name Description

v

The array.

webcpp::document::literal::literal

A date.

Synopsis

literal(date_ms v) noexcept;

Parameters

Name Description

v

The date.

webcpp::document::literal::literal

JavaScript’s null.

Synopsis

literal(null_t v) noexcept;

Parameters

Name Description

v

null.

webcpp::document::literal::literal

JavaScript’s null.

Synopsis

literal(std::nullptr_t v) noexcept;

Parameters

Name Description

v

nullptr.

webcpp::document::literal::literal

An object.

Synopsis

literal(object v);

Parameters

Name Description

v

The object.

webcpp::document::literal::literal

Text in UTF-8, copied.

Synopsis

literal(std::string_view v);

Parameters

Name Description

v

The text.

webcpp::document::literal::literal

Text in UTF-16, copied.

Synopsis

literal(std::u16string_view v);

Parameters

Name Description

v

The text.

webcpp::document::literal::literal

Bytes, copied.

Synopsis

literal(std::span<std::byte const> v);

Parameters

Name Description

v

The bytes.

webcpp::document::literal::literal

Text in UTF-8, copied.

Synopsis

literal(std::string const& v);

Parameters

Name Description

v

The text.

webcpp::document::literal::literal

Text in UTF-8, copied; a null pointer holds null.

Synopsis

literal(char const* v);

Parameters

Name Description

v

The text, terminated by a zero, or a null pointer.

webcpp::document::literal::literal

Text in UTF-16, copied; a null pointer holds null.

Synopsis

literal(char16_t const* v);

Parameters

Name Description

v

The text, terminated by a zero, or a null pointer.

webcpp::document::literal::literal

A boolean, constrained so that a pointer does not convert to it.

Synopsis

template<std::same_as<std::vector<std::byte>> B>
literal(B v);

Template Parameters

Name Description

B

bool.

D

The decimal type.

S

std::u16string, a template parameter, so that it is chosen only for a std::u16string itself.

V

std::vector<std::byte>.

Parameters

Name Description

v

The value.

webcpp::document::literal::literal

A binary64 floating-point value, which only traits whose number is binary store.

Synopsis

template<std::floating_point F>
requires (sizeof(F) == 8)
literal(F v) noexcept;

Template Parameters

Name Description

F

The floating-point type, of 8 bytes.

Parameters

Name Description

v

The value.

webcpp::document::literal::literal

An integer, of any width and signedness, character types aside.

Synopsis

template<std::integral I>
requires (!std::same_as<I, bool> && !std::same_as<I, char> && !std::same_as<I, char8_t> &&
                 !std::same_as<I, char16_t> && !std::same_as<I, char32_t> &&
                 !std::same_as<I, wchar_t>)
literal(I v) noexcept;

Template Parameters

Name Description

I

The integer type.

Parameters

Name Description

v

The value.

webcpp::document::literal::visit

Calls f with the value this literal holds, as one of: null_t, bool, std::int64_t, std::uint64_t, const decimal_digits& (a finite decimal), detail::non_finite_decimal, detail::binary64, std::string_view, std::u16string_view, date_ms, std::span<const std::byte>, const object& or const array&.

Synopsis

template<class F>
decltype(auto)
visit(F&& f) const;

Return Value

What f returns.

Template Parameters

Name Description

F

The callable, invocable with each of those.

Parameters

Name Description

f

The callable.

webcpp::document::null_t

The type of null, JavaScript’s null.

Synopsis

Declared in <webcpp/document/kind.hpp>

struct null_t;

webcpp::document::number_codec

How the traits’ number type is read from JSON text, written as JavaScript writes it, and converted; the primary template has none.

Synopsis

template<class T>
struct number_codec;

Description

Each specialisation has the static members of number_codec<boost::decimal::decimal128_t>.

Specializations

Name Description

number_codec<boost::decimal::decimal128_t>

The number codec of decimal128_t, exact: what is stored is what was sent.

number_codec<double>

The number codec of double, for the traits js_traits, with which numbers round as JavaScript’s do, a number past a double’s range is an infinity, and NaN and the infinities are written null.

Template Parameters

Name Description

T

The number type.

See Also

"Numbers", in the guide.

webcpp::document::number_codec<boost::decimal::decimal128_t>

The number codec of decimal128_t, exact: what is stored is what was sent.

Synopsis

template<>
struct number_codec<boost::decimal::decimal128_t>;

Static Member Functions

Name Description

digits

The digits of a finite value, normalised.

finite

Whether a value is finite.

from_digits

The value of a number’s digits, exactly.

from_int64

The value of a signed 64-bit integer, exactly.

integral

The value as a signed 64-bit integer, when it is one.

parse

Reads a JSON number’s text, exactly.

write

Writes a value as JavaScript’s Number::toString writes its digits.

Static Data Members

Name Description

stores_non_finite

Whether the type stores NaN and the infinities: a decimal refuses them.

webcpp::document::number_codec<boost::decimal::decimal128_t>::digits

The digits of a finite value, normalised.

Synopsis

static
decimal_digits
digits(boost::decimal::decimal128_t const& v) noexcept;

Return Value

Its digits.

Parameters

Name Description

v

The value, finite.

webcpp::document::number_codec<boost::decimal::decimal128_t>::finite

Whether a value is finite.

Synopsis

static
bool
finite(boost::decimal::decimal128_t const& v) noexcept;

Return Value

False for NaN and the infinities.

Parameters

Name Description

v

The value.

webcpp::document::number_codec<boost::decimal::decimal128_t>::from_digits

The value of a number’s digits, exactly.

Synopsis

static
result<boost::decimal::decimal128_t>
from_digits(decimal_digits const& d) noexcept;

Return Value

The value; errc::range when its exponent is outside decimal128_t’s range.

Parameters

Name Description

d

The digits.

webcpp::document::number_codec<boost::decimal::decimal128_t>::from_int64

The value of a signed 64-bit integer, exactly.

Synopsis

static
boost::decimal::decimal128_t
from_int64(int64_t v) noexcept;

Return Value

The value.

Parameters

Name Description

v

The integer.

webcpp::document::number_codec<boost::decimal::decimal128_t>::integral

The value as a signed 64-bit integer, when it is one.

Synopsis

static
std::optional<int64_t>
integral(boost::decimal::decimal128_t const& v) noexcept;

Return Value

The integer, or nothing when v is not an integer or does not fit.

Parameters

Name Description

v

The value.

webcpp::document::number_codec<boost::decimal::decimal128_t>::parse

Reads a JSON number’s text, exactly.

Synopsis

static
result<boost::decimal::decimal128_t>
parse(std::string_view json_number) noexcept;

Return Value

The value; errc::range past 34 significant digits or decimal128_t’s exponents.

Parameters

Name Description

json_number

The text.

webcpp::document::number_codec<boost::decimal::decimal128_t>::write

Writes a value as JavaScript’s Number::toString writes its digits.

Synopsis

static
number_text
write(boost::decimal::decimal128_t const& v) noexcept;

Return Value

The text.

Parameters

Name Description

v

The value, finite.

webcpp::document::number_codec<boost::decimal::decimal128_t>::stores_non_finite

Whether the type stores NaN and the infinities: a decimal refuses them.

Synopsis

inline constexpr static
bool stores_non_finite = false;

webcpp::document::number_codec<double>

The number codec of double, for the traits js_traits, with which numbers round as JavaScript’s do, a number past a double’s range is an infinity, and NaN and the infinities are written null.

Synopsis

template<>
struct number_codec<double>;

Static Member Functions

Name Description

digits

The shortest digits that read back as a finite value.

finite

Whether a value is finite.

from_digits

The double nearest a number’s digits.

from_int64

The double nearest a signed 64-bit integer.

integral

The value as a signed 64-bit integer, when it is one.

parse

Reads a JSON number’s text, rounded to the nearest double, as JSON.parse does.

write

Writes a value as JavaScript’s Number::toString does: the shortest digits that read back.

Static Data Members

Name Description

stores_non_finite

Whether the type stores NaN and the infinities: a double does, and writes them null.

webcpp::document::number_codec<double>::digits

The shortest digits that read back as a finite value.

Synopsis

static
decimal_digits
digits(double const& v) noexcept;

Return Value

Its digits.

Parameters

Name Description

v

The value, finite.

webcpp::document::number_codec<double>::finite

Whether a value is finite.

Synopsis

static
bool
finite(double const& v) noexcept;

Return Value

False for NaN and the infinities.

Parameters

Name Description

v

The value.

webcpp::document::number_codec<double>::from_digits

The double nearest a number’s digits.

Synopsis

static
result<double>
from_digits(decimal_digits const& d) noexcept;

Return Value

The value, an infinity past a double’s range, as JavaScript rounds it; errc::range for more digits than d holds.

Parameters

Name Description

d

The digits.

webcpp::document::number_codec<double>::from_int64

The double nearest a signed 64-bit integer.

Synopsis

static
double
from_int64(int64_t v) noexcept;

Return Value

The value, rounded as JavaScript rounds it.

Parameters

Name Description

v

The integer.

webcpp::document::number_codec<double>::integral

The value as a signed 64-bit integer, when it is one.

Synopsis

static
std::optional<int64_t>
integral(double const& v) noexcept;

Return Value

The integer, or nothing when v is not an integer or does not fit.

Parameters

Name Description

v

The value.

webcpp::document::number_codec<double>::parse

Reads a JSON number’s text, rounded to the nearest double, as JSON.parse does.

Synopsis

static
result<double>
parse(std::string_view json_number) noexcept;

Return Value

The value, an infinity past a double’s range, as JSON.parse makes 1e400 Infinity; errc::syntax for text outside JSON’s grammar.

Parameters

Name Description

json_number

The text.

webcpp::document::number_codec<double>::write

Writes a value as JavaScript’s Number::toString does: the shortest digits that read back.

Synopsis

static
number_text
write(double const& v) noexcept;

Return Value

The text; null for NaN and the infinities.

Parameters

Name Description

v

The value.

webcpp::document::number_codec<double>::stores_non_finite

Whether the type stores NaN and the infinities: a double does, and writes them null.

Synopsis

inline constexpr static
bool stores_non_finite = true;

webcpp::document::number_text

Text of at most 64 characters, held by value, so that writing a number or a date allocates nothing.

Synopsis

struct number_text;

Member Functions

Name Description

view

The text, valid while this object lives.

Data Members

Name Description

chars

The characters, in the first size places.

size

How many characters there are.

webcpp::document::number_text::view

The text, valid while this object lives.

Synopsis

[[nodiscard]]
std::string_view
view() const noexcept;

Return Value

A view of the first size characters.

Note

The return value should not be discarded.

webcpp::document::number_text::chars

The characters, in the first size places.

Synopsis

std::array<char, 64> chars = {};

webcpp::document::number_text::size

How many characters there are.

Synopsis

uint32_t size = 0;

webcpp::document::object

An object, as JavaScript writes {...}: wd::object{{"city", "Recife"}, {"zip", "50000"}}.

Synopsis

class object;

Description

Its members are stored in the order written, by the traits’ key order, and a key written twice keeps the last value, in the first one’s place, as JavaScript does.

Member Functions

Name Description

object [constructor]

Constructors

members

The members, in the order written.

See Also

"Handles", in the guide.

webcpp::document::object::object

Constructors

Synopses

An empty object, {}.

constexpr
object() = default;

An object of these members.

object(std::initializer_list<object_member> members);

Parameters

Name Description

members

The members, in the order written.

webcpp::document::object::object

An empty object, {}.

Synopsis

constexpr
object() = default;

webcpp::document::object::object

An object of these members.

Synopsis

object(std::initializer_list<object_member> members);

Parameters

Name Description

members

The members, in the order written.

webcpp::document::object::members

The members, in the order written.

Synopsis

[[nodiscard]]
std::span<object_member const>
members() const noexcept;

Return Value

A view of them.

Note

The return value should not be discarded.

webcpp::document::object_member

One member of an object literal: its key, in UTF-8, and its value.

Synopsis

struct object_member;

Member Functions

Name Description

object_member [constructor]

A member, written {"key", value} inside an object, as a std::pair is: a constructor rather than an aggregate, which tools would ask to write with designated initializers.

Data Members

Name Description

key

The key, in UTF-8.

value

The value.

webcpp::document::object_member::object_member

A member, written {"key", value} inside an object, as a std::pair is: a constructor rather than an aggregate, which tools would ask to write with designated initializers.

Synopsis

object_member(
    std::string key,
    literal value);

Parameters

Name Description

key

The key, in UTF-8.

value

The value.

webcpp::document::object_member::key

The key, in UTF-8.

Synopsis

std::string key;

webcpp::document::object_member::value

The value.

Synopsis

literal value;

webcpp::document::owned

The storage of a document that owns its image: a buffer of its own, 16-aligned, which grows, twice the size each time, up to its max_bytes.

Synopsis

struct owned;

See Also

"Images", in the guide.

webcpp::document::parse_options

The limits and the revivers of a parse.

Synopsis

struct parse_options;

Data Members

Name Description

max_bytes

The most bytes of text, every chunk counted; past it is errc::capacity.

max_depth

The deepest nesting of arrays and objects; one deeper is errc::capacity.

max_document_bytes

The largest image an owned document grows to, 64 MiB; past it is errc::capacity.

revive_bytes

Whether a string valid in the traits’ bytes policy becomes bytes.

revive_dates

Whether a string in exactly Date.prototype.toJSON’s format becomes a date.

Derived Classes

Name Description

stream_options

The options of a stream: the limits and the revivers of each of its documents, its framing and its policy on errors.

See Also

"Parsing", in the guide.

webcpp::document::parse_options::max_bytes

The most bytes of text, every chunk counted; past it is errc::capacity.

Synopsis

uint64_t max_bytes = std::numeric_limits<std::uint64_t>::max();

webcpp::document::parse_options::max_depth

The deepest nesting of arrays and objects; one deeper is errc::capacity.

Synopsis

uint64_t max_depth = 256;

webcpp::document::parse_options::max_document_bytes

The largest image an owned document grows to, 64 MiB; past it is errc::capacity.

Synopsis

uint64_t max_document_bytes = std::uint64_t{64} << 20U;

Description

Each value takes at least 192 bytes of image, so a short value’s image is a hundred times its text or more: 2 MB of [0,0,...] makes an image of 268 MB. The default is low so that text from the network is refused early: under 1 GiB, 11 MB of such text cost 9 seconds and 2.2 GB of memory before it failed; under 64 MiB, 700 KB fails in 0.4 seconds with 137 MB. A program that parses larger documents raises it.

webcpp::document::parse_options::revive_bytes

Whether a string valid in the traits’ bytes policy becomes bytes.

Synopsis

bool revive_bytes = false;

webcpp::document::parse_options::revive_dates

Whether a string in exactly Date.prototype.toJSON’s format becomes a date.

Synopsis

bool revive_dates = false;

webcpp::document::path_view

The path in the text of a value a selector delivers: each step a member’s key or an element’s index in the text, which on does not renumber.

Synopsis

class path_view;

Description

It is valid during the callback that receives it.

Member Functions

Name Description

path_view [constructor]

Constructors

index

An element’s index in the text.

is_index

Whether a step is an element’s.

key

A member’s key.

size

How many steps the path has.

to_string

The path as a selector of its place, which basic_parser::on parses back: $['items'][3].

See Also

"Selectors", in the guide.

webcpp::document::path_view::path_view

Constructors

Synopses

The path of the root, which has no step.

constexpr
path_view() noexcept = default;

The path of these steps.

explicit
path_view(std::span</* implementation-defined */ const> steps) noexcept;

Parameters

Name Description

steps

The steps, from the root.

webcpp::document::path_view::path_view

The path of the root, which has no step.

Synopsis

constexpr
path_view() noexcept = default;

webcpp::document::path_view::path_view

The path of these steps.

Synopsis

explicit
path_view(std::span</* implementation-defined */ const> steps) noexcept;

Parameters

Name Description

steps

The steps, from the root.

webcpp::document::path_view::index

An element’s index in the text.

Synopsis

[[nodiscard]]
uint64_t
index(std::size_t i) const noexcept;

Return Value

The index.

Note

The return value should not be discarded.

Parameters

Name Description

i

The step, below size, an index’s.

webcpp::document::path_view::is_index

Whether a step is an element’s.

Synopsis

[[nodiscard]]
bool
is_index(std::size_t i) const noexcept;

Return Value

Whether it is an index rather than a key.

Note

The return value should not be discarded.

Parameters

Name Description

i

The step, below size.

webcpp::document::path_view::key

A member’s key.

Synopsis

[[nodiscard]]
std::string_view
key(std::size_t i) const noexcept;

Return Value

The key, in UTF-8.

Note

The return value should not be discarded.

Parameters

Name Description

i

The step, below size, a key’s.

webcpp::document::path_view::size

How many steps the path has.

Synopsis

[[nodiscard]]
std::size_t
size() const noexcept;

Return Value

The count; 0 for the root.

Note

The return value should not be discarded.

webcpp::document::path_view::to_string

The path as a selector of its place, which basic_parser::on parses back: $['items'][3].

Synopsis

[[nodiscard]]
std::string
to_string() const;

Description

A key is quoted, its ' and escaped and every other byte written as it is, control characters included; so it is not RFC 9535’s normalized path, which escapes those too.

Return Value

The text.

Note

The return value should not be discarded.

webcpp::document::pool_block

A lease of one block of an image_pool; the block is its holder’s until the lease is destroyed or assigned, and then goes back to the pool, its bytes zeroed.

Synopsis

class pool_block;

Description

A lease is moved, never copied; a moved-from lease holds no block.

Member Functions

Name Description

pool_block [constructor]

Constructors

~pool_block [destructor]

Gives the block back to its pool, its bytes zeroed.

operator=

Assignment operators

bytes

The block’s bytes, 16-aligned, the pool’s block_bytes of them.

Friends

Name Description

webcpp::document::basic_image_reader

Reads a stream of images, one after another with nothing between them, into the blocks of an image_pool, and opens each in place as a leased_document.

webcpp::document::image_pool

Fixed blocks for images, 16-aligned and zeroed, allocated once: block_bytes times blocks.

See Also

"Images", in the guide.

webcpp::document::pool_block::pool_block

Constructors

Synopses

A lease of no block.

constexpr
pool_block() noexcept = default;

A lease is not copied: a block has one holder.

pool_block(pool_block const&) = delete;

Takes the block of other, which holds none after it.

pool_block(pool_block&& other) noexcept;

Parameters

Name Description

other

The lease.

webcpp::document::pool_block::pool_block

A lease of no block.

Synopsis

constexpr
pool_block() noexcept = default;

webcpp::document::pool_block::pool_block

A lease is not copied: a block has one holder.

Synopsis

pool_block(pool_block const&) = delete;

webcpp::document::pool_block::pool_block

Takes the block of other, which holds none after it.

Synopsis

pool_block(pool_block&& other) noexcept;

Parameters

Name Description

other

The lease.

webcpp::document::pool_block::~pool_block

Gives the block back to its pool, its bytes zeroed.

Synopsis

~pool_block();

webcpp::document::pool_block::operator=

Assignment operators

Synopses

A lease is not copied: a block has one holder.

pool_block&
operator=(pool_block const&) = delete;

Gives this lease’s block back, then takes the block of other, which holds none after it.

pool_block&
operator=(pool_block&& other) noexcept;

Return Value

This lease.

Parameters

Name Description

other

The lease.

webcpp::document::pool_block::operator=

A lease is not copied: a block has one holder.

Synopsis

pool_block&
operator=(pool_block const&) = delete;

webcpp::document::pool_block::operator=

Gives this lease’s block back, then takes the block of other, which holds none after it.

Synopsis

pool_block&
operator=(pool_block&& other) noexcept;

Return Value

This lease.

Parameters

Name Description

other

The lease.

webcpp::document::pool_block::bytes

The block’s bytes, 16-aligned, the pool’s block_bytes of them.

Synopsis

[[nodiscard]]
std::span<std::byte>
bytes() const noexcept;

Return Value

The bytes, empty for a lease of no block.

Note

The return value should not be discarded.

webcpp::document::range_error

JavaScript’s RangeError: a number that does not fit, or an index out of range.

Synopsis

class range_error
    : public error

Base Classes

Name Description

error

What every exception of document derives from: a std::runtime_error holding its code.

Using Declarations

Name Description

range_error

Constructs the exception of a code, as error does.

webcpp::document::range_error::range_error

Constructs the exception of a code, as error does.

Synopsis

using error::range_error;

Introduced Symbols

Name Description

error [constructor]

Constructs the exception of code, whose message is the code’s.

webcpp::document::scalar_codec

How a scalar type is stored in a node: its name in the image’s layout tag, its size, and its bytes, written the same on every target.

Synopsis

template<class T>
struct scalar_codec;

Description

Each specialisation has name, size, store(const T&, std::byte*) and load(const std::byte*); the primary template has none, so a type without one is no stored_scalar.

Specializations

Name Description

scalar_codec<bool>

How a bool is stored: one byte, 0 or 1, since bool’s size is not fixed by the standard.

scalar_codec<date_ms>

How a date_ms is stored: its milliseconds as a std::int64_t.

scalar_codec<boost::decimal::decimal128_t>

How a decimal128_t is stored: its 16 bytes of IEEE 754 BID, least significant first.

scalar_codec<double>

How a double is stored, for the traits js_traits, as its IEEE 754 binary64 bits, least significant first.

scalar_codec<int64_t>

How a std::int64_t is stored: eight bytes, least significant first.

Template Parameters

Name Description

T

The scalar type.

webcpp::document::scalar_codec<bool>

How a bool is stored: one byte, 0 or 1, since bool’s size is not fixed by the standard.

Synopsis

template<>
struct scalar_codec<bool>;

Static Member Functions

Name Description

load

Reads a value written by store.

store

Writes v into out.

Static Data Members

Name Description

name

The codec’s name in the image’s layout tag.

size

How many bytes it takes.

webcpp::document::scalar_codec<bool>::load

Reads a value written by store.

Synopsis

static
bool
load(std::byte const* in) noexcept;

Return Value

The value.

Parameters

Name Description

in

Its size bytes.

webcpp::document::scalar_codec<bool>::store

Writes v into out.

Synopsis

static
void
store(
    bool const& v,
    std::byte* out) noexcept;

Parameters

Name Description

v

The value.

out

Where to write its size bytes.

webcpp::document::scalar_codec<bool>::name

The codec’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "bool";

webcpp::document::scalar_codec<bool>::size

How many bytes it takes.

Synopsis

inline constexpr static
std::size_t size = 1;

webcpp::document::scalar_codec<date_ms>

How a date_ms is stored: its milliseconds as a std::int64_t.

Synopsis

template<>
struct scalar_codec<date_ms>;

Static Member Functions

Name Description

load

Reads a value written by store.

store

Writes v into out.

Static Data Members

Name Description

name

The codec’s name in the image’s layout tag.

size

How many bytes it takes.

webcpp::document::scalar_codec<date_ms>::load

Reads a value written by store.

Synopsis

static
date_ms
load(std::byte const* in) noexcept;

Return Value

The value.

Parameters

Name Description

in

Its size bytes.

webcpp::document::scalar_codec<date_ms>::store

Writes v into out.

Synopsis

static
void
store(
    date_ms const& v,
    std::byte* out) noexcept;

Parameters

Name Description

v

The value.

out

Where to write its size bytes.

webcpp::document::scalar_codec<date_ms>::name

The codec’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "date-ms";

webcpp::document::scalar_codec<date_ms>::size

How many bytes it takes.

Synopsis

inline constexpr static
std::size_t size = 8;

webcpp::document::scalar_codec<boost::decimal::decimal128_t>

How a decimal128_t is stored: its 16 bytes of IEEE 754 BID, least significant first.

Synopsis

template<>
struct scalar_codec<boost::decimal::decimal128_t>;

Static Member Functions

Name Description

load

Reads a value written by store.

store

Writes v into out.

Static Data Members

Name Description

name

The codec’s name in the image’s layout tag.

size

How many bytes it takes.

webcpp::document::scalar_codec<boost::decimal::decimal128_t>::load

Reads a value written by store.

Synopsis

static
boost::decimal::decimal128_t
load(std::byte const* in) noexcept;

Return Value

The value.

Parameters

Name Description

in

Its size bytes.

webcpp::document::scalar_codec<boost::decimal::decimal128_t>::store

Writes v into out.

Synopsis

static
void
store(
    boost::decimal::decimal128_t const& v,
    std::byte* out) noexcept;

Parameters

Name Description

v

The value.

out

Where to write its size bytes.

webcpp::document::scalar_codec<boost::decimal::decimal128_t>::name

The codec’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "decimal128";

webcpp::document::scalar_codec<boost::decimal::decimal128_t>::size

How many bytes it takes.

Synopsis

inline constexpr static
std::size_t size = 16;

webcpp::document::scalar_codec<double>

How a double is stored, for the traits js_traits, as its IEEE 754 binary64 bits, least significant first.

Synopsis

template<>
struct scalar_codec<double>;

Static Member Functions

Name Description

load

Reads a value written by store.

store

Writes v into out.

Static Data Members

Name Description

name

The codec’s name in the image’s layout tag.

size

How many bytes it takes.

webcpp::document::scalar_codec<double>::load

Reads a value written by store.

Synopsis

static
double
load(std::byte const* in) noexcept;

Return Value

The value.

Parameters

Name Description

in

Its size bytes.

webcpp::document::scalar_codec<double>::store

Writes v into out.

Synopsis

static
void
store(
    double const& v,
    std::byte* out) noexcept;

Parameters

Name Description

v

The value.

out

Where to write its size bytes.

webcpp::document::scalar_codec<double>::name

The codec’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "binary64";

webcpp::document::scalar_codec<double>::size

How many bytes it takes.

Synopsis

inline constexpr static
std::size_t size = 8;

webcpp::document::scalar_codec<int64_t>

How a std::int64_t is stored: eight bytes, least significant first.

Synopsis

template<>
struct scalar_codec<int64_t>;

Static Member Functions

Name Description

load

Reads a value written by store.

store

Writes v into out.

Static Data Members

Name Description

name

The codec’s name in the image’s layout tag.

size

How many bytes it takes.

webcpp::document::scalar_codec<int64_t>::load

Reads a value written by store.

Synopsis

static
int64_t
load(std::byte const* in) noexcept;

Return Value

The value.

Parameters

Name Description

in

Its size bytes.

webcpp::document::scalar_codec<int64_t>::store

Writes v into out.

Synopsis

static
void
store(
    int64_t const& v,
    std::byte* out) noexcept;

Parameters

Name Description

v

The value.

out

Where to write its size bytes.

webcpp::document::scalar_codec<int64_t>::name

The codec’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "int64";

webcpp::document::scalar_codec<int64_t>::size

How many bytes it takes.

Synopsis

inline constexpr static
std::size_t size = 8;

webcpp::document::shared_document

A document in a block of a shared_pool, holding one reference to it, as std::shared_ptr holds its object: copying adds a reference, destroying drops one, and the last reference zeroes the block and gives it back to the pool.

Synopsis

template<class Traits = default_traits>
class shared_document;

Description

Every holder reads and writes the same image, in place: a write through one is every holder’s. Whether several holders may write at once is the program’s decision, and so is the synchronisation of writers, with each other and with readers, which the library does not do. A holder that wants a document of its own to change calls make_mutable, which copies the image into a new block when others hold it (copy on write). The references themselves are thread-safe: copies made and dropped on any number of threads and processes at once, as std::shared_ptr’s are. A reference passes to another process as its image’s offset in the region: release gives it up without dropping it, and adopt takes it over.

The document is external: a change that needs more room than its block has returns errc::capacity. A shared document allocates nothing; moving it invalidates its handles.

Member Functions

Name Description

shared_document [constructor]

Constructors

~shared_document [destructor]

Drops the reference; the last one zeroes the block and gives it back.

operator=

Assignment operators

get

get overloads

image

The document’s image, in its block.

make_mutable

Makes the document this holder’s alone to change: the same block when it holds the only reference, else a copy of the image in a new block, this holder’s only, while the other holders keep the document as it was.

offset

The image’s offset from the start of the pool’s region, the same in every process.

operator[]

Subscript operators

release

Gives up this holder’s reference without dropping it, for adopt to take over, in this process or in another that maps the same region.

root

root overloads

seal

Seals the image, as the document’s own seal does: the next change, through any holder, clears it.

sealed

Whether the image carries a seal.

use_count

How many references the document has, in every process.

Static Member Functions

Name Description

adopt

Takes over a reference that release gave up, in this process or in another that maps the same region.

create

Makes a new, empty document, holding null, in a free block of pool, whose image is the whole block.

from_image

Copies an image into a free block of pool, where it then has the whole block’s room.

Template Parameters

Name Description

Traits

The traits of the document.

See Also

"Images", in the guide.

webcpp::document::shared_document::shared_document

Constructors

Synopses

A holder of no document.

shared_document() noexcept = default;

Adds a reference to the document of other.

shared_document(shared_document const& other) noexcept;

Takes the reference of other, which holds none after it.

shared_document(shared_document&& other) noexcept;

Parameters

Name Description

other

The shared document.

webcpp::document::shared_document::shared_document

A holder of no document.

Synopsis

shared_document() noexcept = default;

webcpp::document::shared_document::shared_document

Adds a reference to the document of other.

Synopsis

shared_document(shared_document const& other) noexcept;

Parameters

Name Description

other

The shared document.

webcpp::document::shared_document::shared_document

Takes the reference of other, which holds none after it.

Synopsis

shared_document(shared_document&& other) noexcept;

Parameters

Name Description

other

The shared document.

webcpp::document::shared_document::~shared_document

Drops the reference; the last one zeroes the block and gives it back.

Synopsis

~shared_document();

webcpp::document::shared_document::operator=

Assignment operators

Synopses

Adds a reference to the document of other, then drops this one’s.

shared_document&
operator=(shared_document const& other) noexcept;

Drops this one’s reference, then takes the reference of other, which holds none after it.

Return Value

This shared document.

Parameters

Name Description

other

The shared document.

webcpp::document::shared_document::operator=

Adds a reference to the document of other, then drops this one’s.

Synopsis

shared_document&
operator=(shared_document const& other) noexcept;

Return Value

This shared document.

Parameters

Name Description

other

The shared document.

webcpp::document::shared_document::operator=

Drops this one’s reference, then takes the reference of other, which holds none after it.

Synopsis

shared_document&
operator=(shared_document&& other) noexcept;

Return Value

This shared document.

Parameters

Name Description

other

The shared document.

webcpp::document::shared_document::get

get overloads

Synopses

The document.

The document, read only.

[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;

Return Value

The document, valid while this holder neither moves, drops nor releases it.

webcpp::document::shared_document::get

The document.

Synopsis

Return Value

The document, valid while this holder neither moves, drops nor releases it.

webcpp::document::shared_document::get

The document, read only.

Synopsis

[[nodiscard]]
basic_document<Traits, external> const&
get() const noexcept;

Return Value

The document, valid while this holder neither moves, drops nor releases it.

Note

The return value should not be discarded.

webcpp::document::shared_document::image

The document’s image, in its block.

Synopsis

[[nodiscard]]
std::span<std::byte const>
image() const noexcept;

Return Value

Its bytes, empty for a holder of no document.

Note

The return value should not be discarded.

webcpp::document::shared_document::make_mutable

Makes the document this holder’s alone to change: the same block when it holds the only reference, else a copy of the image in a new block, this holder’s only, while the other holders keep the document as it was.

Synopsis

result<void>
make_mutable() noexcept;

Description

The copy is the image’s bytes, its seal included. Reading the image to copy it races with a holder that writes it at the same moment, which the program’s synchronisation prevents.

Return Value

Success; errc::capacity when a copy is needed and every block is leased, the holder then keeping its reference; errc::type for a holder of no document.

webcpp::document::shared_document::offset

The image’s offset from the start of the pool’s region, the same in every process.

Synopsis

[[nodiscard]]
uint64_t
offset() const noexcept;

Return Value

The offset; 0 for a holder of no document.

Note

The return value should not be discarded.

webcpp::document::shared_document::operator[]

Subscript operators

Synopses

The member key of the document’s value, as root()[key].

basic_value<Traits, external>
operator[](std::string_view key) noexcept;

The member key of the document’s value, read only.

basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;

The element index of the document’s value, as root()[index].

basic_value<Traits, external>
operator[](uint64_t index) noexcept;

The element index of the document’s value, read only.

basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

index

The index.

webcpp::document::shared_document::operator[]

The member key of the document’s value, as root()[key].

Synopsis

basic_value<Traits, external>
operator[](std::string_view key) noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::shared_document::operator[]

The member key of the document’s value, read only.

Synopsis

basic_const_value<Traits, external>
operator[](std::string_view key) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

key

The key, in UTF-8.

webcpp::document::shared_document::operator[]

The element index of the document’s value, as root()[index].

Synopsis

basic_value<Traits, external>
operator[](uint64_t index) noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

index

The index.

webcpp::document::shared_document::operator[]

The element index of the document’s value, read only.

Synopsis

basic_const_value<Traits, external>
operator[](uint64_t index) const noexcept;

Return Value

The handle, which never fails.

Parameters

Name Description

index

The index.

webcpp::document::shared_document::release

Gives up this holder’s reference without dropping it, for adopt to take over, in this process or in another that maps the same region.

Synopsis

[[nodiscard]]
uint64_t
release() noexcept;

Return Value

The image’s offset from the region’s start, which adopt takes; 0 for a holder of no document. The holder holds nothing after it.

Note

The return value should not be discarded.

webcpp::document::shared_document::root

root overloads

Synopses

The handle of the document’s value.

The handle of the document’s value, read only.

[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;

Return Value

  • The handle, which reads and writes.

  • The handle.

webcpp::document::shared_document::root

The handle of the document’s value.

Synopsis

Return Value

The handle, which reads and writes.

webcpp::document::shared_document::root

The handle of the document’s value, read only.

Synopsis

[[nodiscard]]
basic_const_value<Traits, external>
root() const noexcept;

Return Value

The handle.

Note

The return value should not be discarded.

webcpp::document::shared_document::seal

Seals the image, as the document’s own seal does: the next change, through any holder, clears it.

Synopsis

result<void>
seal() noexcept;

Return Value

Success; errc::type for a holder of no document.

webcpp::document::shared_document::sealed

Whether the image carries a seal.

Synopsis

[[nodiscard]]
bool
sealed() const noexcept;

Return Value

Whether it is sealed; false for a holder of no document.

Note

The return value should not be discarded.

webcpp::document::shared_document::use_count

How many references the document has, in every process.

Synopsis

[[nodiscard]]
uint64_t
use_count() const noexcept;

Return Value

The count, which other holders may change at any moment; 0 for a holder of no document.

Note

The return value should not be discarded.

webcpp::document::shared_document::adopt

Takes over a reference that release gave up, in this process or in another that maps the same region.

Synopsis

static
result<shared_document>
adopt(
    shared_pool pool,
    uint64_t offset,
    verify check = verify::seal) noexcept;

Return Value

The document; errc::range for an offset that is no block’s image or a block that no one holds; the errors of the function open, which leave the reference as it was: a receiver that refuses it gives it back with shared_pool::drop.

Parameters

Name Description

pool

The pool, whose region outlives the document.

offset

The image’s offset from the region’s start, which release returned.

check

Whether a sealed image’s bytes are checked against its seal.

webcpp::document::shared_document::create

Makes a new, empty document, holding null, in a free block of pool, whose image is the whole block.

Synopsis

static
result<shared_document>
create(shared_pool pool) noexcept;

Return Value

The document, its only holder; errc::capacity when every block is leased, or when a block is too small for an empty document.

Parameters

Name Description

pool

The pool, whose region outlives the document.

webcpp::document::shared_document::from_image

Copies an image into a free block of pool, where it then has the whole block’s room.

Synopsis

static
result<shared_document>
from_image(
    shared_pool pool,
    std::span<std::byte const> bytes,
    verify check = verify::seal) noexcept;

Description

A sealed image is checked against its seal first; the image grows to its block, which clears its seal unless it already filled one.

Return Value

The document, its only holder; the errors of the function open, and errc::capacity for an image larger than a block or when every block is leased.

Parameters

Name Description

pool

The pool, whose region outlives the document.

bytes

The image, at any address.

check

Whether a sealed image’s bytes are checked against its seal.

webcpp::document::shared_pool

A pool of blocks for shared documents, over a region of memory the program gives: the process heap, or a region of shared memory that several processes map, each at an address of its own.

Synopsis

class shared_pool;

Description

The region holds a 64-byte header, then the blocks, each a 64-byte header, which counts the references to its document, and the room of one image. A shared_pool is a view of the region, copied freely: the pool is the region’s, and every view of it, in any process, is the same pool. Leasing a block and giving one back are lock-free and safe from any number of threads and processes at once; nothing is allocated, and a free block is all zero.

Note

A process that dies holding references leaves their blocks counted for good, as Boost.Interprocess’s shared_ptr does; recovering them is the program’s.

Member Functions

Name Description

shared_pool [constructor]

A view of no pool, which has no block.

available

How many blocks are free.

block_bytes

The room of each block’s image.

blocks

How many blocks the pool has.

drop

Gives up one reference to a document by its image’s offset, without opening the image; the last reference zeroes the block and gives it back.

region

The pool’s bytes, its header and its blocks, at the start of the region it was made over.

Static Member Functions

Name Description

attach

Attaches to a pool another view formatted, in this process or in another that maps the same region at any address.

format

Formats a region as a new pool of as many blocks of block_bytes as it holds, every one free and zero.

region_bytes

The size of a region that holds blocks blocks of block_bytes each.

Static Data Members

Name Description

block_header_bytes

The size of a block’s header, before its image.

header_bytes

The size of the pool’s header, at its region’s start.

Friends

Name Description

webcpp::document::shared_document

A document in a block of a shared_pool, holding one reference to it, as std::shared_ptr holds its object: copying adds a reference, destroying drops one, and the last reference zeroes the block and gives it back to the pool.

See Also

"Images", in the guide.

webcpp::document::shared_pool::shared_pool

A view of no pool, which has no block.

Synopsis

constexpr
shared_pool() noexcept = default;

webcpp::document::shared_pool::available

How many blocks are free.

Synopsis

[[nodiscard]]
std::size_t
available() const noexcept;

Return Value

The count, exact when no block is being leased or given back at the same moment; 0 for a view of no pool.

Note

The return value should not be discarded.

webcpp::document::shared_pool::block_bytes

The room of each block’s image.

Synopsis

[[nodiscard]]
std::size_t
block_bytes() const noexcept;

Return Value

The size, 0 for a view of no pool.

Note

The return value should not be discarded.

webcpp::document::shared_pool::blocks

How many blocks the pool has.

Synopsis

[[nodiscard]]
std::size_t
blocks() const noexcept;

Return Value

The count, 0 for a view of no pool.

Note

The return value should not be discarded.

webcpp::document::shared_pool::drop

Gives up one reference to a document by its image’s offset, without opening the image; the last reference zeroes the block and gives it back.

Synopsis

[[nodiscard]]
result<void>
drop(uint64_t offset) const noexcept;

Description

A receiver that refuses an offset it was handed, because adopt refused its image as damaged or of another layout, or because the program does not want it, gives the reference back so: its image is never read, so a broken header cannot make the block leak.

Return Value

Success; errc::range for an offset that is no block’s image, or a block no one holds, which changes nothing.

Note

The return value should not be discarded.

Parameters

Name Description

offset

The image’s offset from the region’s start, as release returned it.

webcpp::document::shared_pool::region

The pool’s bytes, its header and its blocks, at the start of the region it was made over.

Synopsis

[[nodiscard]]
std::span<std::byte>
region() const noexcept;

Return Value

The bytes, empty for a view of no pool.

Note

The return value should not be discarded.

webcpp::document::shared_pool::attach

Attaches to a pool another view formatted, in this process or in another that maps the same region at any address.

Synopsis

static
result<shared_pool>
attach(std::span<std::byte> region) noexcept;

Return Value

The pool; errc::not_a_document for a region that is not 16-aligned, holds no pool or is shorter than its pool, errc::incompatible_document for a pool of another layout version or byte order.

Parameters

Name Description

region

The region, 16-aligned, which holds the pool from its first byte.

webcpp::document::shared_pool::format

Formats a region as a new pool of as many blocks of block_bytes as it holds, every one free and zero.

Synopsis

static
result<shared_pool>
format(
    std::span<std::byte> region,
    std::size_t block_bytes) noexcept;

Return Value

The pool; errc::range for a region that is not 16-aligned or a size outside those bounds, errc::capacity for a region too small for one block.

Parameters

Name Description

region

The region, 16-aligned, which the program keeps mapped while the pool and its documents are used.

block_bytes

The room of each block’s image, a multiple of 16 and at least 64: the largest image a block holds.

webcpp::document::shared_pool::region_bytes

The size of a region that holds blocks blocks of block_bytes each.

Synopsis

static
result<std::size_t>
region_bytes(
    std::size_t block_bytes,
    std::size_t blocks) noexcept;

Return Value

The size; errc::range for a size or a count outside those bounds, and errc::capacity when the size overflows.

Parameters

Name Description

block_bytes

The room of each block’s image, a multiple of 16 and at least 64.

blocks

How many blocks, at least one and at most 4,294,967,294.

webcpp::document::shared_pool::block_header_bytes

The size of a block’s header, before its image.

Synopsis

inline constexpr static
std::size_t block_header_bytes = detail::shared_block_header_bytes;

webcpp::document::shared_pool::header_bytes

The size of the pool’s header, at its region’s start.

Synopsis

inline constexpr static
std::size_t header_bytes = detail::shared_header_bytes;

webcpp::document::sorted

The object policy that orders keys by their UTF-16 code units, with either encoding, as JavaScript’s sort() and RFC 8785 do, for canonical JSON.

Synopsis

struct sorted;

Static Data Members

Name Description

name

The policy’s name in the image’s layout tag.

webcpp::document::sorted::name

The policy’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "sorted";

webcpp::document::stream_options

The options of a stream: the limits and the revivers of each of its documents, its framing and its policy on errors.

Synopsis

struct stream_options
    : parse_options

Description

Each document is held to the limits a parse holds one text to: max_depth, max_document_bytes, and max_bytes, which counts the bytes of one document from its first byte, not those of the stream.

Base Classes

Name Description

parse_options

The limits and the revivers of a parse.

Data Members

Name Description

framing

How the documents are told apart; documents one after another unless told otherwise.

on_error

What a bad document does; it stops the stream unless told otherwise.

See Also

"Streams of documents", in the guide.

webcpp::document::stream_options::framing

How the documents are told apart; documents one after another unless told otherwise.

Synopsis

webcpp::document::framing framing = framing::concatenated;

webcpp::document::stream_options::on_error

What a bad document does; it stops the stream unless told otherwise.

Synopsis

webcpp::document::on_error on_error = on_error::stop;

webcpp::document::syntax_error

JavaScript’s SyntaxError: malformed JSON text, at a byte offset.

Synopsis

class syntax_error
    : public error

Base Classes

Name Description

error

What every exception of document derives from: a std::runtime_error holding its code.

Member Functions

Name Description

syntax_error [constructor]

Constructs the exception of code at byte offset of the text.

offset

The byte offset of the failure in the text.

webcpp::document::syntax_error::syntax_error

Constructs the exception of code at byte offset of the text.

Synopsis

syntax_error(
    boost::system::error_code code,
    uint64_t offset);

Parameters

Name Description

code

The code of the failure, errc::syntax.

offset

The byte offset of the failure in the text.

webcpp::document::syntax_error::offset

The byte offset of the failure in the text.

Synopsis

[[nodiscard]]
uint64_t
offset() const noexcept;

Return Value

The offset given at construction.

Note

The return value should not be discarded.

webcpp::document::type_error

JavaScript’s TypeError: a value of another kind, or a write through nothing.

Synopsis

class type_error
    : public error

Base Classes

Name Description

error

What every exception of document derives from: a std::runtime_error holding its code.

Using Declarations

Name Description

type_error

Constructs the exception of a code, as error does.

webcpp::document::type_error::type_error

Constructs the exception of a code, as error does.

Synopsis

using error::type_error;

Introduced Symbols

Name Description

error [constructor]

Constructs the exception of code, whose message is the code’s.

webcpp::document::undefined_t

The type of undefined, JavaScript’s undefined, which set takes to erase a member.

Synopsis

Declared in <webcpp/document/kind.hpp>

struct undefined_t;

webcpp::document::utf16

The encoding of strings and keys that stores UTF-16 code units, as JavaScript holds strings.

Synopsis

struct utf16;

Static Data Members

Name Description

name

The encoding’s name in the image’s layout tag.

See Also

"Strings, dates and bytes", in the guide.

webcpp::document::utf16::name

The encoding’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "utf16";

webcpp::document::utf8

The encoding of strings and keys that stores UTF-8, the default.

Synopsis

struct utf8;

Static Data Members

Name Description

name

The encoding’s name in the image’s layout tag.

See Also

"Strings, dates and bytes", in the guide.

webcpp::document::utf8::name

The encoding’s name in the image’s layout tag.

Synopsis

inline constexpr static
std::string_view name = "utf8";

webcpp::document::errc

The errors document reports, as boost::system::error_code values of the category error_category.

Synopsis

enum class errc : int;

Description

The values start at 1, so an error_code of value 0 is no error. An errc converts to an error_code and compares with one: made.error() == wd::errc::syntax.

Note

The values are fixed: they are never reused for a different meaning, so a stored value keeps its meaning across versions.

Members

Name Description

syntax

Malformed JSON text, JavaScript’s SyntaxError.

type

A value of another kind, a write through nothing, or text invalid in the encoding, JavaScript’s TypeError.

range

A number that does not fit or is not exact, an index out of range where JavaScript throws, or a date out of range, JavaScript’s RangeError.

capacity

No room left: a full external buffer, an owned document at its limit, a parser’s limit.

not_a_document

Bytes that are not a document’s image.

incompatible_document

An image of another format version, nonzero reserved bytes, layout, byte order or traits.

damaged_document

A sealed image whose bytes do not match its seal: changed, torn or cut short.

See Also

"Errors", in the guide.

webcpp::document::errc::syntax

Malformed JSON text, JavaScript’s SyntaxError.

Synopsis

syntax = 1

webcpp::document::errc::type

A value of another kind, a write through nothing, or text invalid in the encoding, JavaScript’s TypeError.

Synopsis

type = 2

webcpp::document::errc::range

A number that does not fit or is not exact, an index out of range where JavaScript throws, or a date out of range, JavaScript’s RangeError.

Synopsis

range = 3

webcpp::document::errc::capacity

No room left: a full external buffer, an owned document at its limit, a parser’s limit.

Synopsis

capacity = 4

webcpp::document::errc::not_a_document

Bytes that are not a document’s image.

Synopsis

not_a_document = 5

webcpp::document::errc::incompatible_document

An image of another format version, nonzero reserved bytes, layout, byte order or traits.

Synopsis

incompatible_document = 6

webcpp::document::errc::damaged_document

A sealed image whose bytes do not match its seal: changed, torn or cut short.

Synopsis

damaged_document = 7

webcpp::document::framing

How the documents of a stream are told apart.

Synopsis

enum class framing : uint8_t;

Members

Name Description

concatenated

Documents one after another, any JSON whitespace or none between them: indented JSON and {"a":1} {"b":2} alike.

lines

NDJSON, JSON Lines: one document per line.

rfc7464

JSON text sequences, RFC 7464: each document in a record that 0x1E opens, which ends before the next 0x1E; a document may span lines, and its record holds only whitespace after it.

See Also

"Streams of documents", in the guide.

webcpp::document::framing::concatenated

Documents one after another, any JSON whitespace or none between them: indented JSON and {"a":1} {"b":2} alike.

Synopsis

concatenated

Description

A document ends where its value closes, a top-level number at the next byte that cannot continue it. Nothing tells where a bad document ends, so a stream of this framing stops at its first error.

webcpp::document::framing::lines

NDJSON, JSON Lines: one document per line.

Synopsis

lines

Description

A document ends at its line’s n, which JSON never holds inside a string; JSON whitespace around it, r included, and empty lines are ignored, and the last line may end without one.

webcpp::document::framing::rfc7464

JSON text sequences, RFC 7464: each document in a record that 0x1E opens, which ends before the next 0x1E; a document may span lines, and its record holds only whitespace after it.

Synopsis

rfc7464

webcpp::document::kind

The kind of a value: JavaScript’s typeof, with null, arrays, dates and bytes told apart.

Synopsis

Declared in <webcpp/document/kind.hpp>

enum class kind : uint8_t;

Description

Note

The values are fixed: they are never reused for a different meaning, so a stored value keeps its meaning across versions.

Members

Name Description

undefined

JavaScript’s undefined: never stored; what a handle reads when nothing is there.

null

JavaScript’s null.

boolean

true or false.

number

A number, held as the traits’ integer or number.

string

A string.

date

A Date, held as the traits’ date.

bytes

A Uint8Array: bytes, stored raw.

array

An array.

object

A plain object.

See Also

"Handles", in the guide.

webcpp::document::kind::undefined

JavaScript’s undefined: never stored; what a handle reads when nothing is there.

Synopsis

Declared in <webcpp/document/kind.hpp>

undefined = 0

webcpp::document::kind::null

JavaScript’s null.

Synopsis

Declared in <webcpp/document/kind.hpp>

null = 1

webcpp::document::kind::boolean

true or false.

Synopsis

Declared in <webcpp/document/kind.hpp>

boolean = 2

webcpp::document::kind::number

A number, held as the traits’ integer or number.

Synopsis

Declared in <webcpp/document/kind.hpp>

number = 3

webcpp::document::kind::string

A string.

Synopsis

Declared in <webcpp/document/kind.hpp>

string = 4

webcpp::document::kind::date

A Date, held as the traits’ date.

Synopsis

Declared in <webcpp/document/kind.hpp>

date = 5

webcpp::document::kind::bytes

A Uint8Array: bytes, stored raw.

Synopsis

Declared in <webcpp/document/kind.hpp>

bytes = 6

webcpp::document::kind::array

An array.

Synopsis

Declared in <webcpp/document/kind.hpp>

array = 7

webcpp::document::kind::object

A plain object.

Synopsis

Declared in <webcpp/document/kind.hpp>

object = 8

webcpp::document::on_error

What a stream does with a bad document.

Synopsis

enum class on_error : uint8_t;

Members

Name Description

stop

The first error ends the stream: it is returned again, by every call, until a reset.

skip

A bad document is reported in its place and dropped, and the stream goes on: with framing::lines at the next line, with framing::rfc7464 at the next 0x1E.

See Also

"Streams of documents", in the guide.

webcpp::document::on_error::stop

The first error ends the stream: it is returned again, by every call, until a reset.

Synopsis

stop

webcpp::document::on_error::skip

A bad document is reported in its place and dropped, and the stream goes on: with framing::lines at the next line, with framing::rfc7464 at the next 0x1E.

Synopsis

skip

Description

It is not allowed with framing::concatenated, which has no place to go on from.

webcpp::document::verify

Whether opening an image checks its seal.

Synopsis

enum class verify : uint8_t;

Members

Name Description

seal

A sealed image’s bytes are checked against its seal, a pass over the image, and refused with errc::damaged_document when they differ; an image that is not sealed is not checked.

none

The seal is not checked: for a program that trusts its memory, such as an image it has just written itself.

See Also

"Images", in the guide.

webcpp::document::verify::seal

A sealed image’s bytes are checked against its seal, a pass over the image, and refused with errc::damaged_document when they differ; an image that is not sealed is not checked.

Synopsis

seal

webcpp::document::verify::none

The seal is not checked: for a program that trusts its memory, such as an image it has just written itself.

Synopsis

none

webcpp::document::async_read_document

async_read_document overloads

Synopses

Declared in <webcpp/document/asio.hpp>

Reads the next document of a stream into a buffer of Boost.Asio’s second model, taken by value: the overload of async_read_document for DynamicBuffer_v2.

template<
    class AsyncReadStream,
    class DynamicBuffer,
    class Traits,
    boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value
auto
async_read_document(
    AsyncReadStream& stream,
    DynamicBuffer buffer,
    basic_stream_parser<Traits>& parser,
    CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());

Reads the next document of a stream: from the parser when it holds one, else from the stream, reading only while the parser has none ready.

template<
    class AsyncReadStream,
    class DynamicBuffer,
    class Traits,
    boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires (boost::asio::is_dynamic_buffer_v1<DynamicBuffer>::value &&
             !boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value)
auto
async_read_document(
    AsyncReadStream& stream,
    DynamicBuffer& buffer,
    basic_stream_parser<Traits>& parser,
    CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());

Return Value

What the token makes of the operation.

Template Parameters

Name Description

AsyncReadStream

A stream of Boost.Asio’s AsyncReadStream: a socket, a pipe.

DynamicBuffer

A dynamic buffer of Boost.Asio’s second model, DynamicBuffer_v2; a buffer of both models is one of the second.

Traits

The traits of the documents.

CompletionToken

The completion token.

Parameters

Name Description

stream

The stream.

buffer

A copy of the buffer, which the operation keeps; the storage it names outlives the operation.

parser

The parser of the stream’s framing.

token

The completion token.

See Also

"Streams of documents", in the guide.

webcpp::document::async_read_document

Reads the next document of a stream into a buffer of Boost.Asio’s second model, taken by value: the overload of async_read_document for DynamicBuffer_v2.

Synopsis

Declared in <webcpp/document/asio.hpp>

template<
    class AsyncReadStream,
    class DynamicBuffer,
    class Traits,
    boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value
auto
async_read_document(
    AsyncReadStream& stream,
    DynamicBuffer buffer,
    basic_stream_parser<Traits>& parser,
    CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());

Return Value

What the token makes of the operation.

Template Parameters

Name Description

AsyncReadStream

A stream of Boost.Asio’s AsyncReadStream: a socket, a pipe.

DynamicBuffer

A dynamic buffer of Boost.Asio’s second model, DynamicBuffer_v2; a buffer of both models is one of the second.

Traits

The traits of the documents.

CompletionToken

The completion token.

Parameters

Name Description

stream

The stream.

buffer

A copy of the buffer, which the operation keeps; the storage it names outlives the operation.

parser

The parser of the stream’s framing.

token

The completion token.

See Also

"Streams of documents", in the guide.

webcpp::document::async_read_document

Reads the next document of a stream: from the parser when it holds one, else from the stream, reading only while the parser has none ready.

Synopsis

Declared in <webcpp/document/asio.hpp>

template<
    class AsyncReadStream,
    class DynamicBuffer,
    class Traits,
    boost::asio::completion_token_for<void(boost::system::error_code, basic_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
requires (boost::asio::is_dynamic_buffer_v1<DynamicBuffer>::value &&
             !boost::asio::is_dynamic_buffer_v2<DynamicBuffer>::value)
auto
async_read_document(
    AsyncReadStream& stream,
    DynamicBuffer& buffer,
    basic_stream_parser<Traits>& parser,
    CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());

Description

It is a composed operation of Boost.Asio, which takes any completion token: a callback, asio::use_awaitable, asio::deferred, boost::cobalt::use_op. Its signature is void(boost::system::error_code, basic_document<Traits>), and it completes with the next document; with the error of a document dropped, under on_error::skip, after which the next call goes on with the stream; with the error that stopped the stream, under on_error::stop, every time; with asio::error::eof once the stream ended and the parser’s finish delivered what it held; or with the error of a read. On an error the document is empty. A completion is never called inside this function: one that needs no read is dispatched to the handler’s immediate executor, else posted to the stream’s.

The buffer is a dynamic buffer of either of Boost.Asio’s models, and the bytes it already holds, left by an earlier read of the same stream, are the parser’s before any read. A buffer of the first model, DynamicBuffer_v1 (asio::streambuf, Beast’s multi_buffer, whatever the pieces of its data), is taken by reference and used through it: the caller keeps it alive until the operation completes. A buffer of the second model, DynamicBuffer_v2 (grow, shrink, data(pos, n): what asio::dynamic_buffer makes of a string, or a vector, also when BOOST_ASIO_NO_DYNAMIC_BUFFER_V1 leaves out the first model) is taken by value, as Boost.Asio’s own reads take it, and the operation keeps its copy: the caller keeps alive the storage it names, the string, and the function never needs the original. A buffer of both models is a buffer of the second, as in Boost.Asio. One operation at a time reads a stream, its buffer and its parser, which outlive it.

Return Value

What the token makes of the operation.

Template Parameters

Name Description

AsyncReadStream

A stream of Boost.Asio’s AsyncReadStream: a socket, a pipe.

DynamicBuffer

A dynamic buffer of Boost.Asio’s first model, DynamicBuffer_v1, and not of the second.

Traits

The traits of the documents.

CompletionToken

The completion token.

Parameters

Name Description

stream

The stream.

buffer

The buffer each read fills; the parser consumes what it holds before each read.

parser

The parser of the stream’s framing.

token

The completion token.

See Also

"Streams of documents", in the guide.

webcpp::document::async_read_image

Reads the next image of a stream into a block of a pool and opens it there, with no copy and no allocation.

Synopsis

Declared in <webcpp/document/asio.hpp>

template<
    class AsyncReadStream,
    class Traits = default_traits,
    boost::asio::completion_token_for<void(boost::system::error_code, leased_document<Traits>)> CompletionToken = boost::asio::default_completion_token_t<AsyncReadStream::executor_type>>
auto
async_read_image(
    AsyncReadStream& stream,
    image_pool& pool,
    CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncReadStream::executor_type>());

Description

The image’s header is read into the block and checked before the rest of the image follows into the same block, which then holds the document. It reads exactly the bytes the reader asks for, so the stream is left at the end of the image it completed.

It is a composed operation of Boost.Asio, which takes any completion token. Its signature is void(boost::system::error_code, leased_document<Traits>): on success the document, which gives its block back when it goes; on error the empty lease (leased_document::empty), with the error: errc::capacity when every block of the pool is leased, in which case the stream is not read, errc::not_a_document or errc::damaged_document for an image the reader refuses, or the error of a read. The end of the stream is asio::error::eof, and it too is reported as errc::capacity when every block is leased: the reader needs a block to learn that the stream ended, so a pool that is full hides the end until a block is given back.

Images are trusted input: this path is for peers the program trusts.

Return Value

What the token makes of the operation.

Template Parameters

Name Description

AsyncReadStream

A stream of Boost.Asio’s AsyncReadStream: a socket, a pipe.

Traits

The traits of the images.

CompletionToken

The completion token.

Parameters

Name Description

stream

The stream.

pool

The pool whose blocks the image is read into; it outlives the operation and the document.

token

The completion token.

See Also

"Images on the wire", in the guide.

webcpp::document::async_write_image

Writes a document’s image to a stream as one buffer, after sealing the document.

Synopsis

Declared in <webcpp/document/asio.hpp>

template<
    class AsyncWriteStream,
    class Traits,
    class Storage,
    boost::asio::completion_token_for<void(boost::system::error_code)> CompletionToken = boost::asio::default_completion_token_t<AsyncWriteStream::executor_type>>
auto
async_write_image(
    AsyncWriteStream& stream,
    basic_document<Traits, Storage>& document,
    CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>());

Description

The image carries a checksum of its bytes, which the reader checks, so a byte changed on the way is refused with errc::damaged_document. The document is changed by the seal, which is why it is taken by non-const reference.

It is a composed operation of Boost.Asio, which takes any completion token. Its signature is void(boost::system::error_code): the error of the seal, which writes nothing, or of the write. The caller keeps the document alive until the operation completes: the write reads its image.

Return Value

What the token makes of the operation.

Template Parameters

Name Description

AsyncWriteStream

A stream of Boost.Asio’s AsyncWriteStream: a socket, a pipe.

Traits

The traits of the document.

Storage

The storage of the document, owned or external.

CompletionToken

The completion token.

Parameters

Name Description

stream

The stream.

document

The document to send, sealed by the operation.

token

The completion token.

See Also

"Sealed images", in the guide.

webcpp::document::async_write_images

Writes the images of a range of documents to a stream in one gather write, after sealing every document of the range.

Synopsis

Declared in <webcpp/document/asio.hpp>

template<
    class AsyncWriteStream,
    /* implementation-defined */ Range,
    boost::asio::completion_token_for<void(boost::system::error_code, std::size_t)> CompletionToken = boost::asio::default_completion_token_t<AsyncWriteStream::executor_type>>
auto
async_write_images(
    AsyncWriteStream& stream,
    Range& documents,
    CompletionToken&& token = boost::asio::default_completion_token_t<typename AsyncWriteStream::executor_type>());

Description

The seals come first: the first document that fails to seal completes the operation with its error, having written nothing. Otherwise the images are written as one gather write, which Boost.Asio sends with writev and continues over partial writes, with no copy of their bytes. The bytes are read in place, so the documents must not change until the operation completes.

It is a composed operation of Boost.Asio, which takes any completion token. Its signature is void(boost::system::error_code, std::size_t): the bytes written, the sum of the images’ sizes on success. On an error it is the bytes that reached the stream, which may end inside an image: the whole documents sent are the leading ones whose image sizes, added in order, do not exceed it. An empty range completes with 0 and writes nothing.

Return Value

What the token makes of the operation.

Template Parameters

Name Description

AsyncWriteStream

A stream of Boost.Asio’s AsyncWriteStream: a socket, a pipe.

Range

The range of documents: its elements are lvalues of documents, or of any type that seals its image and shows its bytes, such as a leased or a shared document.

CompletionToken

The completion token.

Parameters

Name Description

stream

The stream.

documents

The range, read by this call only: the operation seals its documents, and the caller keeps them alive until it completes. A temporary range is refused.

token

The completion token.

See Also

"Images on the wire", in the guide.

webcpp::document::check_image

Checks that bytes are an image this build reads, reading nothing past the header before its size is known, and that a sealed image’s bytes match its seal.

Synopsis

result<void>
check_image(
    std::span<std::byte const> bytes,
    uint32_t layout,
    verify check = verify::seal) noexcept;

Description

A seal is checked first, before anything else its header states is trusted, so that a change anywhere in a sealed image, its magic and its size included, is reported as damage. Then the magic, the format version and the byte order, before the size, which an image of another version or byte order states in another way; then the reserved bytes and the layout.

Return Value

Success; errc::not_a_document for bytes shorter than the header or than the size it states, a size below 64, or another magic; errc::incompatible_document for another format version, byte order, nonzero reserved bytes or layout; errc::damaged_document for a sealed image whose bytes do not match its seal, or are fewer than its size, and for a seal that is neither zero nor marked.

Parameters

Name Description

bytes

The bytes, at any address.

layout

The layout tag this build expects.

check

Whether a sealed image’s bytes are checked against its seal.

webcpp::document::error_category

The category of every errc.

Synopsis

boost::system::error_category const&
error_category() noexcept;

Description

Its name() is "webcpp.document", and its message(value) the human message of the error, "no room left" for @ref errc::capacity, or "unknown webcpp.document error" for a value errc does not define.

Return Value

The category, a constant with a fixed identity.

webcpp::document::format

Formats a buffer as a new, empty document, holding null.

Synopsis

template<class Traits = default_traits>
result<basic_document<Traits, external>>
format(std::span<std::byte> buffer) noexcept;

Return Value

The document; errc::capacity when the buffer is too small for an empty document or is not 16-aligned.

Template Parameters

Name Description

Traits

The traits.

Parameters

Name Description

buffer

The buffer, 16-aligned, which the document uses in place and its user keeps alive.

See Also

"Images", in the guide.

webcpp::document::from_decimal

The digits of a finite value of a decimal type, normalised: every member of a cohort, 1.5 and 1.50, has the same digits.

Synopsis

template<decimal_type Decimal>
decimal_digits
from_decimal(Decimal v) noexcept;

Return Value

Its digits.

Template Parameters

Name Description

Decimal

The decimal type.

Parameters

Name Description

v

The value, which must be finite.

webcpp::document::from_decimal128

The digits of a finite decimal128_t, normalised, as from_decimal gives them.

Synopsis

decimal_digits
from_decimal128(boost::decimal::decimal128_t v) noexcept;

Return Value

Its digits.

Parameters

Name Description

v

The value, which must be finite.

webcpp::document::from_image

Copies an image into a new owned document.

Synopsis

template<class Traits = default_traits>
result<basic_document<Traits, owned>>
from_image(
    std::span<std::byte const> bytes,
    uint64_t max_bytes = default_max_bytes,
    verify check = verify::seal) noexcept;

Description

A sealed image is checked against its seal first, as open checks it; the copy keeps the seal, its bytes being the image’s.

Return Value

The document; the errors of open, and errc::capacity when the image is larger than max_bytes.

Template Parameters

Name Description

Traits

The traits the image was written with.

Parameters

Name Description

bytes

The image, at any address.

max_bytes

The size the copy may grow to.

check

Whether a sealed image’s bytes are checked against its seal.

See Also

"Images", in the guide.

webcpp::document::from_int64

The digits of a signed 64-bit integer.

Synopsis

decimal_digits
from_int64(int64_t v) noexcept;

Return Value

Its digits, without trailing zeros: 1000 is {false, "1", 1, 4}.

Parameters

Name Description

v

The integer.

webcpp::document::from_uint64

The digits of an unsigned 64-bit integer.

Synopsis

decimal_digits
from_uint64(uint64_t v) noexcept;

Return Value

Its digits, without trailing zeros.

Parameters

Name Description

v

The integer.

webcpp::document::make_error_code

Makes the error_code of an errc, as its implicit conversion does.

Synopsis

boost::system::error_code
make_error_code(errc e) noexcept;

Return Value

e as an error_code of the category error_category.

Parameters

Name Description

e

The error.

webcpp::document::open

Opens the image already in a buffer, in place.

Synopsis

template<class Traits = default_traits>
result<basic_document<Traits, external>>
open(
    std::span<std::byte> buffer,
    verify check = verify::seal) noexcept;

Description

A sealed image is checked against its seal first, a pass over the image, unless check is verify::none.

Return Value

The document; errc::not_a_document for bytes that are not an image or a buffer that is not 16-aligned, errc::incompatible_document for an image of another format version, reserved bytes, layout, byte order or traits, errc::damaged_document for a sealed image whose bytes do not match its seal.

Template Parameters

Name Description

Traits

The traits the image was written with.

Parameters

Name Description

buffer

The buffer, which the document uses in place and its user keeps alive.

check

Whether a sealed image’s bytes are checked against its seal.

See Also

"Images", in the guide.

webcpp::document::parse

JSON.parse(text): reads JSON text into a new owned document.

Synopsis

template<class Traits = default_traits>
result<basic_document<Traits, owned>>
parse(
    std::string_view text,
    parse_options const& o = {}) noexcept;

Description

It accepts what RFC 8259 and JSON.parse accept: no comments, no trailing commas, whitespace of space, tab, line feed and carriage return only. A key written twice keeps the last value in the first one’s place, as JavaScript does. A lone surrogate, "\ud800", is refused with either encoding, since Boost.JSON’s parser, which document uses, cannot deliver one.

Return Value

The document; errc::syntax for malformed text, errc::range for a number the traits’ number refuses, errc::capacity past a limit.

Template Parameters

Name Description

Traits

The traits of the document.

Parameters

Name Description

text

The text.

o

The limits and the revivers.

See Also

"Parsing", in the guide.

webcpp::document::parse_into

Reads JSON text into a new document in a buffer its user owns, as parse does.

Synopsis

template<class Traits = default_traits>
result<basic_document<Traits, external>>
parse_into(
    std::span<std::byte> buffer,
    std::string_view text,
    parse_options const& o = {}) noexcept;

Return Value

The document; the errors of parse, and errc::capacity when the buffer is full.

Template Parameters

Name Description

Traits

The traits of the document.

Parameters

Name Description

buffer

The buffer, 16-aligned, which the document uses in place.

text

The text.

o

The limits and the revivers.

webcpp::document::parse_or_throw

JSON.parse(text), throwing on failure as JavaScript does; only with exceptions.

Synopsis

template<class Traits = default_traits>
requires detail::exceptions_enabled<Traits>
basic_document<Traits, owned>
parse_or_throw(
    std::string_view text,
    parse_options const& o = {});

Exceptions

Name Thrown on

syntax_error

For malformed text, with the byte offset where it was found.

range_error

For a number the traits’ number refuses.

capacity_error

Past a limit.

Return Value

The document.

Template Parameters

Name Description

Traits

The traits of the document.

Parameters

Name Description

text

The text.

o

The limits and the revivers.

See Also

"The sugar that throws", in the guide.

webcpp::document::scan_number

Reads a JSON number’s text exactly, never through a double.

Synopsis

result<decimal_digits>
scan_number(std::string_view json_number) noexcept;

Description

The text follows JSON’s grammar, -? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE][+-]? [0-9]+)?. Leading zeros and trailing zeros are dropped, so that 1.50, 1.5 and 15e-1 read the same.

Return Value

Its digits; errc::syntax for text outside the grammar, errc::range for a 35th significant digit, or for a number other than zero whose exponent or point leaves std::int32_t. Zero is zero whatever its exponent: 0e2147483648 reads as 0.

Parameters

Name Description

json_number

The number’s text, as a JSON parser delivers it.

webcpp::document::stringify

stringify overloads

Synopses

JSON.stringify(v, null, indent): writes a value as JSON text.

template<
    class Traits,
    class Storage>
std::string
stringify(
    basic_const_value<Traits, Storage> v,
    uint32_t indent = 0);

JSON.stringify(doc, null, indent): writes a document’s value as JSON text.

template<
    class Traits,
    class Storage>
std::string
stringify(
    basic_document<Traits, Storage> const& d,
    uint32_t indent = 0);

Return Value

  • The text; empty when the value is undefined, since JSON.stringify(undefined) returns no text. The standard library’s std::bad_alloc is its only failure.

  • The text, as stringify of its root writes it.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

Parameters

Name Description

v

The value.

indent

The spaces of each level, clamped to 10; 0 writes compactly.

d

The document.

See Also

"Writing JSON", in the guide.

webcpp::document::stringify

JSON.stringify(v, null, indent): writes a value as JSON text.

Synopsis

template<
    class Traits,
    class Storage>
std::string
stringify(
    basic_const_value<Traits, Storage> v,
    uint32_t indent = 0);

Description

Keys come out in the policy’s order; strings are escaped as JSON.stringify escapes them; numbers are written as JavaScript writes them; a date as Date.prototype.toJSON writes it; bytes by the traits’ bytes policy.

Return Value

The text; empty when the value is undefined, since JSON.stringify(undefined) returns no text. The standard library’s std::bad_alloc is its only failure.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

Parameters

Name Description

v

The value.

indent

The spaces of each level, clamped to 10; 0 writes compactly.

See Also

"Writing JSON", in the guide.

webcpp::document::stringify

JSON.stringify(doc, null, indent): writes a document’s value as JSON text.

Synopsis

template<
    class Traits,
    class Storage>
std::string
stringify(
    basic_document<Traits, Storage> const& d,
    uint32_t indent = 0);

Return Value

The text, as stringify of its root writes it.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

Parameters

Name Description

d

The document.

indent

The spaces of each level, clamped to 10; 0 writes compactly.

webcpp::document::stringify_to

stringify_to overloads

Synopses

Writes a value as stringify does, in pieces of at most chunk bytes, to a sink.

template<
    class Traits,
    class Storage,
    class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
    basic_const_value<Traits, Storage> v,
    Sink&& sink,
    uint32_t indent = 0,
    uint64_t chunk = default_chunk);

Writes a document’s value as stringify does, in pieces of at most chunk bytes, to a sink.

template<
    class Traits,
    class Storage,
    class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
    basic_document<Traits, Storage> const& d,
    Sink&& sink,
    uint32_t indent = 0,
    uint64_t chunk = default_chunk);

Return Value

  • Success, or the first failure the sink returned, which stops the writing; an exception the sink throws propagates. The document is unchanged either way.

  • Success, or the first failure the sink returned.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

Sink

A callable taking std::string_view and returning void or result<void>.

Parameters

Name Description

v

The value.

sink

The sink.

indent

The spaces of each level, clamped to 10; 0 writes compactly.

chunk

The largest piece, at least 1.

d

The document.

See Also

"Writing JSON", in the guide.

webcpp::document::stringify_to

Writes a value as stringify does, in pieces of at most chunk bytes, to a sink.

Synopsis

template<
    class Traits,
    class Storage,
    class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
    basic_const_value<Traits, Storage> v,
    Sink&& sink,
    uint32_t indent = 0,
    uint64_t chunk = default_chunk);

Return Value

Success, or the first failure the sink returned, which stops the writing; an exception the sink throws propagates. The document is unchanged either way.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

Sink

A callable taking std::string_view and returning void or result<void>.

Parameters

Name Description

v

The value.

sink

The sink.

indent

The spaces of each level, clamped to 10; 0 writes compactly.

chunk

The largest piece, at least 1.

See Also

"Writing JSON", in the guide.

webcpp::document::stringify_to

Writes a document’s value as stringify does, in pieces of at most chunk bytes, to a sink.

Synopsis

template<
    class Traits,
    class Storage,
    class Sink>
requires detail::sink<std::remove_reference_t<Sink>>
result<void>
stringify_to(
    basic_document<Traits, Storage> const& d,
    Sink&& sink,
    uint32_t indent = 0,
    uint64_t chunk = default_chunk);

Return Value

Success, or the first failure the sink returned.

Template Parameters

Name Description

Traits

The document’s traits.

Storage

The document’s storage.

Sink

A callable taking std::string_view and returning void or result<void>.

Parameters

Name Description

d

The document.

sink

The sink.

indent

The spaces of each level, clamped to 10; 0 writes compactly.

chunk

The largest piece, at least 1.

webcpp::document::to_decimal

Converts a number to a decimal type, exactly.

Synopsis

template<decimal_type Decimal>
result<Decimal>
to_decimal(decimal_digits const& d) noexcept;

Return Value

The value; errc::range when Decimal cannot hold it exactly: more digits than its precision, or an exponent outside its range.

Template Parameters

Name Description

Decimal

The decimal type.

Parameters

Name Description

d

The number.

webcpp::document::to_decimal128

Converts a number to decimal128_t, exactly, as to_decimal does.

Synopsis

result<boost::decimal::decimal128_t>
to_decimal128(decimal_digits const& d) noexcept;

Return Value

The value; errc::range when its exponent is outside decimal128_t’s range.

Parameters

Name Description

d

The number.

webcpp::document::to_int64

Converts a number to a signed 64-bit integer, when it is one.

Synopsis

std::optional<int64_t>
to_int64(decimal_digits const& d) noexcept;

Return Value

Its value when it is an integer from INT64_MIN to INT64_MAX, else nothing.

Parameters

Name Description

d

The number.

webcpp::document::to_uint64

Converts a number to an unsigned 64-bit integer, when it is one.

Synopsis

std::optional<uint64_t>
to_uint64(decimal_digits const& d) noexcept;

Return Value

Its value when it is an integer from 0 to UINT64_MAX, else nothing.

Parameters

Name Description

d

The number.

webcpp::document::traits_tag

The traits’ identity in the image’s layout tag: an FNV-1a hash, 32 bits, of each scalar codec’s name and size (boolean, integer, number, date), then the names of the string, bytes, object and array policies, each followed by a zero byte.

Synopsis

template<class Traits>
constexpr
uint32_t
traits_tag() noexcept;

Return Value

The hash: two traits that store or order differently have different tags.

Template Parameters

Name Description

Traits

The traits.

webcpp::document::write_javascript

Writes a number as JavaScript’s Number::toString writes it, ECMA-262, radix 10.

Synopsis

number_text
write_javascript(decimal_digits const& d) noexcept;

Description

With k significant digits and the decimal point after n of them:

| Condition | Written as | Example | | --- | --- | --- | | k <= n <= 21 | the digits, then n - k zeros | 100000000000000000000 | | 0 < n <= 21 | the digits with a point after n | 123.45 | | -6 < n <= 0 | 0., -n zeros, the digits | 0.000001 | | otherwise | one digit, . and the rest if any, e, the sign, n - 1 | 1.5e+300 |

Return Value

Its text, which never holds more than 64 characters.

Parameters

Name Description

d

The number.

See Also

"Numbers", in the guide.

webcpp::document::operator==

Equality operators

Synopses

Whether two documents hold equal values, structurally, key order aside.

bool
operator==(
    basic_document const& a,
    basic_document const& b) noexcept;

Whether two values are equal, structurally: the same kind; numbers by value (1, 1.0 and 1e0 are equal); strings, dates and bytes by value; arrays element by element; objects with the same keys, each with an equal value, in any order.

bool
operator==(
    basic_const_value const& a,
    basic_const_value const& b) noexcept;

Return Value

  • Whether a.root() == b.root().

  • Whether their values are equal.

Parameters

Name Description

a

A document.

b

A document.

webcpp::document::operator==

Whether two documents hold equal values, structurally, key order aside.

Synopsis

bool
operator==(
    basic_document const& a,
    basic_document const& b) noexcept;
Note

This function is a hidden friend of basic_document, declared only inside that class: it can be found only through argument-dependent lookup.

Return Value

Whether a.root() == b.root().

Parameters

Name Description

a

A document.

b

A document.

webcpp::document::operator==

Whether two values are equal, structurally: the same kind; numbers by value (1, 1.0 and 1e0 are equal); strings, dates and bytes by value; arrays element by element; objects with the same keys, each with an equal value, in any order.

Synopsis

bool
operator==(
    basic_const_value const& a,
    basic_const_value const& b) noexcept;
Note

This function is a hidden friend of basic_const_value, declared only inside that class: it can be found only through argument-dependent lookup.

Description

Two undefined are equal; JavaScript has no deep equality, and this one is the document’s.

Return Value

Whether their values are equal.

Parameters

Name Description

a

A handle.

b

A handle.

webcpp::document::byte_order_mark

The byte-order mark of the header, as the machine that wrote it writes this number.

Synopsis

inline constexpr uint32_t byte_order_mark = 0x01020304;

webcpp::document::default_chunk

The size of the pieces stringify_to hands its sink, unless told otherwise.

Synopsis

inline constexpr uint64_t default_chunk = 4096;

webcpp::document::default_max_bytes

The largest image an owned document grows to unless told otherwise: 1 GiB.

Synopsis

inline constexpr uint64_t default_max_bytes = std::uint64_t{1} << 30U;

webcpp::document::format_version

The version of the image’s format that this library writes and opens.

Synopsis

inline constexpr uint32_t format_version = 1;

Description

The library raises it when the layout of an image changes, and then provides a migration from the previous version; none exists yet, since 1 is the first. It is the library’s constant, not an option: a new Boost release does not change it, and the format test watches that the layout the Boost libraries give the image has not changed.

See Also

"Images", in the guide.

webcpp::document::image_magic

The first eight bytes of every image: WCPPDOC and a zero byte.

Synopsis

inline constexpr std::array<char, 8> image_magic = {'W', 'C', 'P', 'P', 'D', 'O', 'C', '\0'};

webcpp::document::null

JavaScript’s null, which set stores, as nullptr does.

Synopsis

Declared in <webcpp/document/kind.hpp>

inline constexpr null_t null = {};

webcpp::document::undefined

JavaScript’s undefined: v.set(wd::undefined) erases a member, as delete does, and stores null in an element.

Synopsis

Declared in <webcpp/document/kind.hpp>

inline constexpr undefined_t undefined = {};

webcpp::document::decimal_type

The decimal types of Boost.Decimal a document converts to and from: decimal32_t, decimal64_t, decimal128_t and their fast variants.

Synopsis

template<class T>
concept decimal_type = std::same_as<T, boost::decimal::decimal32_t> || std::same_as<T, boost::decimal::decimal64_t> ||
    std::same_as<T, boost::decimal::decimal128_t> ||
    std::same_as<T, boost::decimal::decimal_fast32_t> ||
    std::same_as<T, boost::decimal::decimal_fast64_t> ||
    std::same_as<T, boost::decimal::decimal_fast128_t>;

Template Parameters

Name Description

T

The type.

webcpp::document::document_traits

What a document’s traits must give: the scalar types, each a stored_scalar, a number_codec for number, and one of the encodings and policies.

Synopsis

template<class T>
concept document_traits = std::same_as<typename T::boolean, bool> && std::same_as<typename T::integer, std::int64_t> &&
    std::same_as<typename T::date, date_ms> && stored_scalar<typename T::number> &&
    requires { number_codec<typename T::number>::stores_non_finite; } &&
    (std::same_as<typename T::string, utf8> || std::same_as<typename T::string, utf16>) &&
    (std::same_as<typename T::bytes, base64> || std::same_as<typename T::bytes, base64url> ||
     std::same_as<typename T::bytes, javascript_bytes>) &&
    (std::same_as<typename T::object, javascript_order> ||
     std::same_as<typename T::object, insertion_order> ||
     std::same_as<typename T::object, sorted>) &&
    std::same_as<typename T::array, dense>;

Description

This version requires bool, std::int64_t and date_ms for boolean, integer and date; number is the traits’ choice.

Template Parameters

Name Description

T

The traits.

See Also

"Traits", in the guide.

webcpp::document::stored_scalar

A type a node can store as a scalar: trivially copyable, with a scalar_codec whose size is 1, 2, 4, 8 or 16 bytes.

Synopsis

template<class T>
concept stored_scalar = std::is_trivially_copyable_v<T> &&
    requires {
        { scalar_codec<T>::name } -> std::convertible_to<std::string_view>;
        { scalar_codec<T>::size } -> std::convertible_to<std::size_t>;
    } &&
    (scalar_codec<T>::size == 1 || scalar_codec<T>::size == 2 || scalar_codec<T>::size == 4 ||
     scalar_codec<T>::size == 8 || scalar_codec<T>::size == 16);

Template Parameters

Name Description

T

The type.

WEBCPP_DOCUMENT_NO_DANGLING

Marks a class that names a place in a document and owns what it needs to find it again, a handle or a range of a container’s children; empty outside GCC 14 and later, whose -Wdangling-reference it quiets.

Synopsis

#define WEBCPP_DOCUMENT_NO_DANGLING

WEBCPP_DOCUMENT_NO_EXCEPTIONS

Defined when document is built without exceptions: when webcpp has none (the compiler has none, or the developer defined WEBCPP_NO_EXCEPTIONS for every webcpp library), or when the developer defines it alone.

Synopsis

#define WEBCPP_DOCUMENT_NO_EXCEPTIONS

Description

document’s core never throws, so the macro changes none of it. It removes the members that throw (value::operator=, as, as_string, as_bool, parse_or_throw) and the exception types: a call to one of those members does not compile, and the compiler names detail::exceptions_enabled.

See Also

"Testing", in the guide.

Appendix A: Changelog

What changed in document in each version of webcpp, newest first, one line per change. "Unreleased" is what the next version brings. The release notes of every version are on webcpp’s index page.

Unreleased

webcpp 0.1.0

  • Added the library: a JSON document that behaves as JavaScript does.

  • Added traits that choose the stored types, and JavaScript’s key order.

  • Added handles, array methods and JavaScript-style sugar that throws, compiled only with exceptions.

  • Added object and array views that the standard algorithms read, with key-ordered queries.

  • Added stringify, parse, and selectors that stream in bounded memory.

  • Added streams of documents: NDJSON, JSON text sequences, concatenated JSON, Asio, Cobalt and xactor.

  • Added one relocatable binary image per document, with reserve, shrink_to_fit and compact.

  • Added sealed images, and Asio readers and writers that open them in place in pools of blocks.

  • Added shared documents, reference counted over shared memory, with copy on write.

  • Added a JavaScript module: exact decimals, key ranges, images, streams and Workers.