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.
}| API | Contract |
|---|---|
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. |
FlowerClientOptions | Optional adminToken (sent only on admin calls), fetch and queryUrls (nonempty; changes routing, not consistency). |
RequestOptions | Optional signal: AbortSignal. |
MutationOptions | Adds 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. |
DeploymentOptions | Optional requestId, signal and preparation: "online" | "blocking". Blocking pauses writes during preparation; reads keep working. |
DeploymentReceipt | revision, value and duplicate. |
Bundle | hash (SHA-256 of the JavaScript) and javascript. |
FlowerRequestInit | The 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.
| API | Contract |
|---|---|
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. |
WatchOptions | Optional signal, maxEventBytes (17 MiB), maxValueBytes (16 MiB), maxPatchOperations (256). These are client-side limits only. intervalMs is rejected; use watchPoll. |
WatchPollOptions | Optional signal and intervalMs (default 250, range 1–2,147,483,647). |
WatchSnapshot<Value = Json> | type: "snapshot", sequence, revision, value. |
WatchPatch | type: "patch", sequence, baseSequence, revision, patch. |
WatchDelta<Value = Json> | WatchSnapshot or WatchPatch. |
JsonPatchOperation | add/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 withWATCH_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.
| API | Contract |
|---|---|
createHttp2Transport(options = {}): Http2Transport | Node only. Uses h2c for http:// and verified TLS for https://, with one shared session per origin. URLs can’t contain credentials. |
Http2TransportOptions | Optional 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. |
Http2Transport | fetch 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.