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";
const client = new FlowerClient("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 [first, second] = await Promise.all([
client.query("counter.get", "visits"),
client.query("counter.get", "clicks"),
]);
const updates = client.watch("counter.get", "visits");- Queries and new watches rotate through
queryUrls. A watch stays on the node it started on. - Mutations, deploys and
call()use the main URL. Any node works; it forwards writes to the leader. - The client doesn't retry, discover nodes or change consistency for you.
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 authorization still runs 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 authorization callbacks. 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", (ctx, id: string) =>
ctx.get(counters, id) ?? 0,
{ consistency: "replica-local" },
);
// Include "counter.local": getLocal in define({ http: ... }).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. - 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";
const transport = createHttp2Transport({ requestTimeoutMs: 10_000 });
const client = new FlowerClient("http://127.0.0.1:7101", {
fetch: transport.fetch,
});
try {
console.log(await client.call("counter.increment", "visits", {
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 retries, 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
Use client.watch to get a query's value now and again every time it changes.
const stop = new AbortController();
for await (const { revision, value } of client.watch("counter.get", "visits", {
signal: stop.signal,
})) {
console.log(revision, value);
// Break the loop or call stop.abort() to disconnect.
}- The first update is the full value. After that the server sends only changes, and the SDK rebuilds the value for you.
- Watches may skip intermediate values when changes come fast. Use a watch for current state, not as an event log.
- Nothing is sent if the value doesn't change.
- Each subscriber is authorized on its own, and the watch closes if its credentials expire or are revoked.
- A deploy or principal change ends the stream with
WATCH_SCOPE_CHANGED. - Streams don't reconnect themselves. After an error, start a new watch on any node; it begins with a fresh full value. There is no replay of missed events.
- 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-side size limits, on both 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.
Use watchPoll(query, args, {intervalMs, signal}) if you want polling instead.
Raw deltas with watchDeltas
POST /v1/watch takes {name, args?} 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.
Common errors
| Response | What to do |
|---|---|
404 METHOD_NOT_FOUND | Use a name the current deployment exposes. Removing a name also rejects old retries through it. |
409 REQUEST_ID_REUSED | That ID was already used with different arguments. |
409 REVISION_CONFLICT | Read the current state, then decide whether to send a new operation. |
422 EVALUATION_FAILED | Your code threw. Nothing committed. |
503 UNAVAILABLE | Wait for the leader or queue to recover, then retry with the same request ID. |