Reference · 04

Client and transports.

FlowerClient, live watches, and the HTTP/2 transport.

FlowerClient

Use FlowerClient to call your methods from Node or a browser. Point it at any cluster member.

Use any member; spread reads across replicas
import { FlowerClient, FlowerError } from "@flower-js/sdk/client";

const db = new FlowerClient("http://node-1:7101", {
  queryUrls: ["http://node-1:7101", "http://node-2:7101", "http://node-3:7101"],
});
const requestId = crypto.randomUUID(); // Persist before sending.
try {
  await db.mutate("orders.save", { id: "42", cents: 900 }, { requestId });
} catch (error) {
  if (error instanceof FlowerError) console.error(error.status, error.code);
  // After an uncertain response, retry the same body and requestId.
}
APIContract
new FlowerClient(url?, options?)Default URL http://127.0.0.1:7101. Any member works; the server forwards writes to the leader. Readonly url is the normalized URL.
constructor(url?, options: FlowerClientOptions = {})Creates a client. Makes no network calls.
initialize(members, options?): Promise<void>Admin. Bootstraps a new cluster from node ID → host:port. Use once, on a fresh group only; never on a restarted member.
deploy(bundle, {requestId?, signal?, preparation?}?): Promise<DeploymentReceipt>Admin. Deploys a bundle. By default (online) the old code keeps serving during preparation. A write in the meantime returns DEPLOYMENT_CONFLICT: retry with the same request ID, or use preparation: "blocking".
query<Args, Value>(name, args = null, options?): Promise<QueryResult<Value>>Calls an exposed query. Rotates through queryUrls. The deployed code decides freshness.
mutate<Args, Value>(name, args = null, options?): Promise<MutationResult<Value>>Calls an exposed mutation or transaction, with a request ID and optional expected revision.
call<Args, Value>(name, args = null, options?): Promise<MutationResult<Value>>Calls any exposed alias; the deployed code decides its kind. Always uses the main URL, even for queries.
FlowerClientOptionsOptional adminToken (sent only on admin calls), fetch and queryUrls (nonempty; changes routing, not consistency).
RequestOptionsOptional signal: AbortSignal.
MutationOptionsAdds optional requestId and expectedRevision. Without a request ID, each call gets a new random one, so it isn’t safe to retry.
QueryResult<Value = Json>revision and value.
MutationResult<Value = Json>Adds duplicate: boolean. A retried request returns its original result and revision.
DeploymentOptionsOptional requestId, signal and preparation: "online" | "blocking". Blocking pauses writes during preparation; reads keep working.
DeploymentReceiptrevision, value and duplicate.
Bundlehash (SHA-256 of the JavaScript) and javascript.
FlowerRequestInitThe fetch options Flower uses: POST, headers, string body, optional signal.
FlowerFetch(url, init) => Promise<Response>. A custom fetch must support streaming bodies for watches.
new FlowerError(message, status = 0, code = "FLOWER_ERROR")An Error with status and code, thrown for HTTP failures. Network errors, aborts and bad arguments throw ordinary errors, so don’t assume every failure is a FlowerError.
constructor(message, status = 0, code = "FLOWER_ERROR")Sets the message, HTTP status (0 if none) and code.

Retrying safely

  • A timed-out or lost mutation may still have committed. Retry with the same request ID, arguments and expected revision. Save the request ID before sending.
  • Same ID, different body: REQUEST_ID_REUSED. Stale expected revision: REVISION_CONFLICT. Server queue full: 503.
  • Aborting stops waiting. It doesn’t undo a commit.
  • Old request IDs never run again, even after their results are dropped. See retry retention.
  • The SDK doesn’t retry or fail over for you. If a member is unreachable, try another one yourself.

Watches

A watch streams a query’s latest value as it changes, over SSE.

APIContract
watch<Args, Value>(name, args = null, options?): AsyncGenerator<QueryResult<Value>>Yields full values, rebuilt from a snapshot plus patches. Skips updates whose value didn’t change. Each yielded value is your own copy.
watchDeltas<Args, Value>(name, args = null, options?): AsyncGenerator<WatchDelta<Value>>Yields raw snapshot and patch events for you to apply. Starts with a snapshot, whose sequence may be above zero. No replay or auto-reconnect.
watchPoll<Args, Value>(name, args = null, options?): AsyncGenerator<QueryResult<Value>>Polls instead of streaming. Yields on every revision change, even if the value is equal. The interval starts after each query finishes.
WatchOptionsOptional signal, maxEventBytes (17 MiB), maxValueBytes (16 MiB), maxPatchOperations (256). These are client-side limits only. intervalMs is rejected; use watchPoll.
WatchPollOptionsOptional signal and intervalMs (default 250, range 1–2,147,483,647).
WatchSnapshot<Value = Json>type: "snapshot", sequence, revision, value.
WatchPatchtype: "patch", sequence, baseSequence, revision, patch.
WatchDelta<Value = Json>WatchSnapshot or WatchPatch.
JsonPatchOperationadd/replace with a value, or remove, at a JSON pointer path. An empty path means the whole value.
Watch a query alias
import { FlowerClient } from "@flower-js/sdk/client";

const db = new FlowerClient("http://node-1:7101");
const stop = new AbortController();
for await (const { revision, value } of db.watch("pizza.board", null, { signal: stop.signal })) {
  console.log(revision, value);
  // break or stop.abort() releases the subscription.
}
  • A watch gives you the latest value, not every change. Intermediate revisions can be skipped. It isn’t an event log.
  • Fresh watches read like fresh queries. Replica-local watches can lag, and can go back in time after reconnecting elsewhere.
  • Watches don’t reconnect. After an error or abort, start a new watch and expect a new snapshot.
  • A redeploy or change of caller identity ends the stream with WATCH_SCOPE_CHANGED. Malformed stream data ends it with WATCH_PROTOCOL_ERROR.
  • Break the loop or abort the signal to close the stream.
  • Each subscriber is authorized on every refresh. Remember that one watched value can combine data from many records.
  • There’s no fixed limit on watch count. Memory, sockets and update rate set the practical limit. Slow consumers get batched updates and are eventually closed.

HTTP/2 transport

In Node, use one pooled HTTP/2 transport to share connections across many requests and watches.

APIContract
createHttp2Transport(options = {}): Http2TransportNode only. Uses h2c for http:// and verified TLS for https://, with one shared session per origin. URLs can’t contain credentials.
Http2TransportOptionsOptional requestTimeoutMs (30,000), idleTimeoutMs (30,000), maxSessions (16), maxRequestBytes (8 MiB), maxResponseBytes (64 MiB), and ca (PEM roots that replace Node’s defaults). Certificate checks can’t be disabled.
Http2Transportfetch and close().
transport.fetch(url, init): Promise<Response>Pass to FlowerClient as fetch. Streams SSE; buffers JSON up to maxResponseBytes.
transport.close(): Promise<void>Cancels open streams and closes sessions. Safe to call twice. Await it in finally.
Reuse one transport across client instances
import { FlowerClient } from "@flower-js/sdk/client";
import { createHttp2Transport } from "@flower-js/sdk/http2";

const transport = createHttp2Transport();
try {
  const db = new FlowerClient("http://localhost:7101", { fetch: transport.fetch });
  console.log(await db.query("pizza.board"));
} finally {
  await transport.close();
}
  • The request timeout covers the whole response, except for SSE, where it covers only the headers.
  • No HTTP/1 fallback, redirects, retries or compressed responses.
  • Transport failures throw errors with H2_* codes. After a connection failure, a mutation’s outcome is unknown, so retry it with the same request ID.