Spread reads across replicas
Give the client a list of queryUrls to run queries and watches on every node. Reads stay fresh by default: each one is confirmed with a quorum, so it needs a working majority.
import { FlowerClient } from "@flower-js/sdk";
import type counter from "./counter.ts";
const client = new FlowerClient<typeof counter>("http://127.0.0.1:7101", {
queryUrls: [
"http://127.0.0.1:7101",
"http://127.0.0.1:7102",
"http://127.0.0.1:7103",
],
});
const [visits, clicks] = await Promise.all([
client.query("counter.get", "visits"),
client.query("counter.get", "clicks"),
]);
console.log(visits.value.count, clicks.value.count);- Queries and new watches rotate through
queryUrls. A watch stays on the node it started on;subscribemoves to the next one when it reconnects. - Mutations, deploys and
call()use the main URL. Any node works; it forwards writes to the leader. - The client retries only when you pass
retry. It doesn't discover nodes or change consistency.
Query result caching
Flower reuses a query's result while the records, indexes and code it read are unchanged. This never makes a read staler: fresh queries still confirm with a quorum first, and access checks still run on every request. Queries that read ctx.now() are never cached.
FLOWER_QUERY_CACHE_BYTES: cache size per logical database. Default 16 MiB; 0 disables it.FLOWER_QUERY_FLIGHT_BYTES: memory for sharing identical concurrent reads. Default 512 KiB; 0 disables it.FLOWER_QUERY_WORKERS: bounds fast cache lookups and access checks. Heavy query work sharesFLOWER_PREPARATION_WORKERSwith writes and watches.
Allow stale reads (opt in)
If a query can tolerate lag, declare it replica-local. It then reads the node's own data without asking a quorum, and keeps working during a network partition.
const getLocal = query("internal.counter.local", {
args: v.string({ min: 1 }), consistency: "replica-local",
}, (ctx, id) => ctx.get(counters, id) ?? 0);
// Expose it: define({ http: { "counter.local": getLocal } }).Replica-local reads can be stale, with no bound. Data, code and the list of public methods may all lag. Two calls on different nodes can return revisions that go backwards.
- The policy lives in the application. HTTP callers can't change it.
- Only queries can declare it. Leave it out, or use
linearizable, for fresh reads. - Apps with access checks or managed keys read fresh anyway.
- A restarted node may need a quorum once before it serves these reads.
Use HTTP/2 from Node.js
Use the HTTP/2 transport to send many requests over one connection per node.
import { FlowerClient } from "@flower-js/sdk";
import { createHttp2Transport } from "@flower-js/sdk/http2";
import type counter from "./counter.ts";
const transport = createHttp2Transport({ requestTimeoutMs: 10_000 });
const client = new FlowerClient<typeof counter>("http://127.0.0.1:7101", {
fetch: transport.fetch,
});
try {
console.log(await client.mutate("counter.increment", { id: "visits", by: 1 }, {
requestId: "visit-over-h2-001",
signal: AbortSignal.timeout(5_000),
}));
} finally {
await transport.close();
}- Import it from
@flower-js/sdk/http2. It works in Node.js only. - Options and defaults:
requestTimeoutMs(30 s),idleTimeoutMs(30 s),maxRequestBytes(8 MiB),maxResponseBytes(64 MiB),maxSessions(16). - For watches, the timeout covers only the response headers. Your signal or
transport.close()ends the stream. - Close the transport when done; that cancels open streams. It never redirects or downgrades.
- Cancelling doesn't undo a mutation that already committed. After an uncertain result, retry with the same request ID.
The server port speaks HTTP/1.1 and cleartext HTTP/2 (h2c). To check it with curl:
curl --http2-prior-knowledge -i http://127.0.0.1:7101/healthFor TLS, set FLOWER_TLS_CERT_FILE, FLOWER_TLS_KEY_FILE and FLOWER_TLS_CA_FILE. The transport's optional ca option adds trusted roots; certificate checks can't be turned off.
Watch live queries
client.subscribe gives you a query's value now and again every time it changes, and keeps going through disconnects.
const stop = new AbortController();
for await (const { value, revision, reset } of client.subscribe("counter.get", "visits", {
signal: stop.signal,
})) {
if (reset) console.log("fresh snapshot");
console.log(revision, value.count);
// Break the loop or call stop.abort() to disconnect.
}
const { value } = await client.waitUntil("counter.get", "visits", (counter) => counter.count >= 10);- The first update is the full value. After that the server sends only changes, and the SDK rebuilds the value for you.
- Updates may skip intermediate values when changes come fast. Use them for current state, not as an event log. Nothing is sent if the value doesn't change.
- After a disconnect, a stall (no bytes for 45 s; the server sends a keepalive every 15 s) or a transient error,
subscribereconnects with backoff and marks the next valuereset: true. Other errors end the loop. - Pass
monotonic: trueto skip values older than one already delivered, such as from a lagging replica. waitUntilresolves with the first value that passes your test, reconnecting as needed.- Each subscriber is authorized on its own, and the stream closes if its credentials expire or are revoked.
- A deploy or principal change ends a stream with
WATCH_SCOPE_CHANGED.subscribereconnects and delivers a fresh snapshot. - Fresh watches recheck on a timer as well as on commits:
FLOWER_WATCH_REFRESH_MS, default 250 ms. - A client that can't accept an update within the send timeout (5 s by default) is disconnected.
client.watch is the single-connection version: it yields { revision, value } and ends at the first disconnect. watchPoll(alias, args, { intervalMs, signal }) polls instead.
Client-side size limits apply to subscribe, watch and watchDeltas. Raise them if your server allows larger results:
maxValueBytes: 16 MiBmaxEventBytes: 17 MiBmaxPatchOperations: 256
The CLI takes the same limits: watch --max-value-bytes 33554432 --max-event-bytes 34603008 --max-patch-operations 512.
Raw deltas with watchDeltas
POST /v1/watch takes {name, args?, credentials?} and returns server-sent events. The first is a snapshot; later ones are RFC 6902 JSON Patch deltas, or a new snapshot when that is cheaper.
watchDeltas gives you those events with a type field. Snapshots carry sequence, revision, value. Patches carry sequence, baseSequence, revision, patch. A patch applies only to the event whose sequence matches its baseSequence. Sequences can start above zero and skip after a reset. A value that depends on time can change without a new revision. Error events carry the method's failure, like HTTP errors.
For error codes, see Handle failures.