Guide · 05

Read from any replica.

Spread queries across replicas, choose between fresh and fast reads, and watch values change live.

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.

client-replicas.ts · checkout root
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 shares FLOWER_PREPARATION_WORKERS with 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.

Application · optional local read
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.

client-h2.ts · checkout root
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:

With an HTTP/2-enabled curl
curl --http2-prior-knowledge -i http://127.0.0.1:7101/health

For 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.

One query, kept current
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 MiB
  • maxEventBytes: 17 MiB
  • maxPatchOperations: 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

Common errors
ResponseWhat to do
404 METHOD_NOT_FOUNDUse a name the current deployment exposes. Removing a name also rejects old retries through it.
409 REQUEST_ID_REUSEDThat ID was already used with different arguments.
409 REVISION_CONFLICTRead the current state, then decide whether to send a new operation.
422 EVALUATION_FAILEDYour code threw. Nothing committed.
503 UNAVAILABLEWait for the leader or queue to recover, then retry with the same request ID.