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.
- Rust
- Await every engine:
tinystore::aioruns on any executor.
- Await every engine:
- Metrics
- Read a Prometheus exporter: one parser in the core for every SDK.
- SQL
- A row and its job in one batch:
queue.addingcommits with the statements. - Stop a slow query: cancel it and get its connection back.
- A row and its job in one batch:
- Records
- Print less than you keep: a level for the store and one for the console.
- Config
- Where each setting came from: every value it was given, as data.
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:
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.
Print less than you keep
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})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.rpslet 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`)The same works for the reads of metrics and records. Stopping a slow query has the details.
Changed names
records.dbmoves to a new format at the first open, and 0.1.0 refuses the file afterwards.- Each
explainline gainsgiven. In Rust,config::Line,Explainedand the newOriginare#[non_exhaustive]. In Go,Lineholds 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
undefinedis 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
CancelledErrorinstead of the signal'sDOMException.
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
uint64andfloat32values 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.1pip install tinyshed-tinystore==0.1.1go get tinyshed.org/tinystore@v0.1.1cargo add tinystore@0.1.1Every commit since 0.1.0 is in the GitHub release.