TinyStore

Releases / TinyStore 0.1.1

TinyStore 0.1.1

TinyStore 0.1.1 came out on 24 October 2026, four days after the first release. Rust programs can await every engine, metrics reads a Prometheus exporter's text, and a database's batch can add a job together with its rows. A store's logger can print less than it keeps, and a slow query stops when its caller gives up.

Await every engine

The async feature adds tinystore::aio. Each call that reads, writes or waits returns a future, and TinyStore brings no runtime of its own, so it runs on tokio, async-std or smol:

rust
use tinystore::aio::Store;

let store = Store::open("data", Options::default())?;
let stock = store.bucket::<i64>("stock").open()?;
stock.set("sku-1", &10).await?;

A read stops when you drop its future, and a write runs on the store's own threads, never on the thread that polls it. Bun, Node and Python were already awaited, and Go blocks a goroutine, so this part is Rust only. Rust with async has the details.

Read a Prometheus exporter

ingestPrometheus reads the text that an exporter such as node_exporter serves, and writes all of its series in one call:

const text = await (await fetch('http://web-1:9100/metrics')).text()
await store.metrics.ingestPrometheus(text, { labels: { instance: 'web-1' } })
text = httpx.get("http://web-1:9100/metrics").text
await store.metrics.ingest_prometheus(text, labels={"instance": "web-1"})
resp, err := http.Get("http://web-1:9100/metrics")
body, err := io.ReadAll(resp.Body)
err = metrics.IngestPrometheus(ctx, store, body, metrics.Labels{"instance": "web-1"})
let stats = store.metrics()?;
stats.prometheus().label("instance", "web-1").ingest(&body)?;

It reads Prometheus' text format and OpenMetrics, and a store now holds more than 100,000 series when it opens with max_series (maxSeries, MaxSeries).

A row and its job in one batch

A queue that you open from a database keeps its jobs in the database's file. Its adding gives a job that a batch writes together with the statements:

const indexing = db.queue<{ noteId: number }>('indexing')

await db.batch([
	sql`update notes set body = ${body} where id = ${noteId}`,
	indexing.adding({ noteId }), // written with the row, or not at all
])
indexing = db.queue("indexing", Index)

await db.batch([
    sql("update notes set body = ? where id = ?", body, note_id),
    indexing.adding(Index(note_id=note_id)),  # written with the row, or not at all
])
indexing, err := jobs.OpenQueue[Index](db, "indexing")

err = db.Batch(ctx,
	sqldb.SQL(`update notes set body = ? where id = ?`, body, noteID),
	indexing.Adding(Index{NoteID: noteID}), // written with the row, or not at all
)
let indexing = db.queue::<Index>("indexing").open()?;

db.batch([
    sql!("update notes set body = ? where id = ?", body, note_id).into(),
    indexing.adding(&Index { note_id }), // written with the row, or not at all
])?;

A worker sees the job only after the commit, so it never reads a row that isn't there yet.

A store's logger takes a level for each place: store is the least level kept, and console the least level printed. The terminal shows warnings, and a query still finds every line from info:

const log = store.logger('api', { level: { store: 'info', console: 'warn' } })
log = store.logger("api", level={"store": "info", "console": "warning"})
handler := records.NewHandler(store, "api", records.Levels{Store: slog.LevelInfo, Console: slog.LevelWarn})
let levels = Levels { store: Some(Level::INFO), console: Some(Level::WARN) };
let log = store.logger("api").level(levels).layer()?;

LOG_CONSOLE_LEVEL sets the console's part from the environment, beside LOG_LEVEL.

Where each setting came from

Each line of explain now has given: every value the setting was given, newest first, each with where it came from as data. Use it to show a setting's history in your own words:

const rps = config.explain().find(line => line.path === 'limits.rps')
const [own, under] = rps?.given ?? []
rps = next(line for line in config.explain() if line.path == "limits.rps")
own, under = rps.given[0], rps.given[1]
own, under := line.Given[0], line.Given[1] // line: the explain line of limits.rps
let explained = config.explain();
let rps = explained.settings.iter().find(|line| line.path == "limits.rps").unwrap();

Explain shows what each origin holds.

Stop a slow query

When a visitor closes a tab or a request passes its timeout, cancel the query. TinyStore stops its SQLite statement at once and gives the connection back:

const signal = AbortSignal.any([request.signal, AbortSignal.timeout(2000)])
const totals = await withSignal(signal, () => db.all`select user_id, sum(total) from orders group by user_id`)
async with asyncio.timeout(2):
    totals = await db.all("select user_id, sum(total) from orders group by user_id")
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
totals, err := sqldb.All[Total](ctx, db, `select user_id, sum(total) from orders group by user_id`)
let token = CancellationToken::timeout(Duration::from_secs(2));
let totals: Vec<Total> = token.run(|| db.all("select user_id, sum(total) from orders group by user_id"))?;

The same works for the reads of metrics and records. Stopping a slow query has the details.

Changed names

  • records.db moves to a new format at the first open, and 0.1.0 refuses the file afterwards.
  • Each explain line gains given. In Rust, config::Line, Explained and the new Origin are #[non_exhaustive]. In Go, Line holds a slice and can no longer be compared with ==.
  • Python's store.log_handler(level=...) sets the store logger's level, and a level that TinyStore doesn't know, such as 'CRITICAL', is refused.
  • In Bun and Node, a field whose value is undefined is left out of a log line instead of written as "undefined".
  • A store's directory that is a file is an invalid error in every SDK, and a cancelled call in Bun and Node throws CancelledError instead of the signal's DOMException.

Fixes

  • A connection that is lost while a handle opens sends the write again on the next connection, instead of answering that its outcome is unknown.
  • A store's close cancels its subscriptions, loops, tails and watches in Bun, Node and Python, which kept a child process busy for ten seconds.
  • A Windows server's secret and named pipe are readable by its own user only.
  • Go's uint64 and float32 values in KV read back the same in Rust.
  • Nodes and their copies got eight fixes to leases, links and fencing.

Install

bun add @tinyshed/tinystore@0.1.1
pip install tinyshed-tinystore==0.1.1
go get tinyshed.org/tinystore@v0.1.1
cargo add tinystore@0.1.1

Every commit since 0.1.0 is in the GitHub release.

Edit this page on GitHub Updated 26 October 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results