Reference · 06

Client and transports.

FlowerClient for your methods, FlowerAdmin for operators, live values, and the HTTP/2 transport.

FlowerClient

Use FlowerClient to call your methods from Node or a browser. Point it at any cluster member, and give it your app's type.

Typed calls, replica reads, automatic retries
import { FlowerClient, FlowerError } from "@flower-js/sdk/client";
import type counter from "./counter.ts";

const db = new FlowerClient<typeof counter>("http://node-1:7101", {
  queryUrls: ["http://node-1:7101", "http://node-2:7101", "http://node-3:7101"],
  retry: true,
});
try {
  await db.mutate("counter.increment", { id: "visits", by: 1 }, { requestId: "visit-42" });
} catch (error) {
  if (!(error instanceof FlowerError) || !error.failure) throw error;
  console.error(error.failure.code, error.failure.details);
}
APIContract
new FlowerClient<App>(url?, options?)Default URL http://127.0.0.1:7101. With App set to typeof app, aliases, arguments and results are typed; without it, calls are untyped. 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. Throws TypeError for a non-HTTP URL or an empty queryUrls.
query(alias, args?, options?): Promise<QueryResult<V>>Calls an exposed query on the next of queryUrls. The deployed code decides freshness. Omit args for methods that take null.
mutate(alias, args?, options?): Promise<MutationResult<V>>Calls an exposed mutation or transaction, with a request ID and optional expected revision. A transaction returns { results }, plus value when its plan has one.
call(alias, args?, options?): Promise<MutationResult<V>>Calls any exposed alias; the deployed code decides its kind. Always uses the main URL, even for queries. A transaction returns { results }, plus value when its plan has one.
FlowerClientOptionsOptional fetch; queryUrls (nonempty; changes routing, not consistency); credentials (JSON, or a possibly async function called for every request and connection); retry (the default for every call); boundedRetries (see retention).
RequestOptionsOptional signal, credentials (overrides the client’s for this call) and retry.
MutationOptionsAdds optional requestId and expectedRevision. Without a request ID, the call gets a new one, reused across its own retries only.
RetryPolicyretry: true or an object: attempts (8, including the first), until (no retry starts after this epoch millisecond; the first attempt always runs), initialDelayMs (250, doubling with jitter), maxDelayMs (30,000), timeoutMs (20,000 per attempt) and retryable (default isTransient). Mutations keep one request ID across attempts.
QueryResult<V = Json>revision and value.
MutationResult<V = Json>Adds duplicate: boolean. A retried request returns its original result and revision.
new FlowerError(message, status = 0, code = "FLOWER_ERROR", failure?)Thrown for HTTP and protocol failures. status is the HTTP status (0 if none), code is Flower’s (EVALUATION_FAILED, FORBIDDEN, TRANSACTION_ABORTED, UNAVAILABLE…), and failure is the method’s, access check’s or participant’s { code, message, details? }. Network errors and aborts throw ordinary errors.
constructor(message, status = 0, code = "FLOWER_ERROR", failure?)Sets the message, status, code and, when given, failure.
isTransient(error): booleanTrue for network errors, timeouts, stalled or ended watches, 408, 425, 429 and 5xx. False for aborts and for anything with a failure: a method’s own failure is never transient.
backoff(attempt, initialDelayMs = 250, maxDelayMs = 30_000): numberA jittered delay between half and all of initialDelayMs · 2^attempt, capped at maxDelayMs.
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.

Retrying safely

  • A timed-out or lost mutation may still have committed. Retry with the same request ID, arguments and expected revision; retry does this for you. Save the request ID first if the process itself may restart.
  • 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 client doesn’t move mutations to another member. If the main URL is unreachable, point a client at another one.

Live values

Watches stream a query’s latest value as it changes, over SSE. subscribe keeps one going through disconnects.

APIContract
subscribe(alias, args?, options?): AsyncGenerator<Update<V>>Yields full values. After a disconnect, a stall or a transient error it reconnects with backoff, rotating queryUrls, and marks the next value reset. Other errors end it.
waitUntil(alias, args?, predicate = Boolean, options?): Promise<Update<V>>Resolves with the first value that passes predicate, reconnecting as needed. Queries that take null may omit args.
watch(alias, args?, options?): AsyncGenerator<QueryResult<V>>One connection. Yields full values, rebuilt from a snapshot plus patches, and skips unchanged ones. Each value is your own copy. Ends when the connection does.
watchDeltas(alias, args?, options?): AsyncGenerator<WatchDelta<V>>Yields raw snapshot and patch events for you to apply. Starts with a snapshot, whose sequence may be above zero. No replay or reconnect.
watchPoll(alias, args?, options?): AsyncGenerator<QueryResult<V>>Polls instead of streaming. Yields on every revision change, even if the value is equal. The interval starts after each query finishes.
WatchOptionsOptional signal, credentials, maxEventBytes (17 MiB), maxValueBytes (16 MiB) and maxPatchOperations (256). The limits are client-side only.
SubscribeOptionsWatchOptions plus reconnect (default true, or { initialDelayMs, maxDelayMs }), stallMs (45,000: reconnect when not even a keepalive arrives) and monotonic (skip values older than one already delivered).
Update<V = Json>revision, value and reset: true for the first value of each connection, after which intermediate values may have been skipped.
WatchPollOptionsRequestOptions plus intervalMs (default 250, range 1–2,147,483,647).
WatchSnapshot<V = Json>type: "snapshot", sequence, revision, value.
WatchPatchtype: "patch", sequence, baseSequence, revision, patch.
WatchDelta<V = Json>WatchSnapshot or WatchPatch.
JsonPatchOperationadd/replace with a value, or remove, at a JSON pointer path. An empty path means the whole value.
Follow a query
import { FlowerClient } from "@flower-js/sdk/client";
import type pizza from "./pizza.ts";

const db = new FlowerClient<typeof pizza>("http://node-1:7101");
const stop = new AbortController();
for await (const { revision, value, reset } of db.subscribe("pizza.board", null, { signal: stop.signal })) {
  console.log(revision, reset, value.orders, value.mushroom);
  // 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; monotonic hides that.
  • A redeploy or change of caller identity ends a stream with WATCH_SCOPE_CHANGED, which subscribe treats as a reconnect. Malformed stream data ends it with WATCH_PROTOCOL_ERROR.
  • Error events carry the method’s failure, like HTTP errors.
  • 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.

FlowerAdmin

Operator calls live on their own client, which sends the admin token on every request. Keep it out of browsers and application bundles.

Build and deploy
import { FlowerAdmin } from "@flower-js/sdk/client";
import { buildBundle } from "@flower-js/sdk/bundle";

const admin = new FlowerAdmin("http://node-1:7101", { adminToken: process.env.FLOWER_ADMIN_TOKEN });
const bundle = await buildBundle("app.ts");
console.log(await admin.deploy(bundle, { requestId: `deploy-${bundle.hash}` }));
APIContract
new FlowerAdmin(url?, options?)Operator endpoints: deployment, cluster, partitions, keys, retention and transaction closure. Default URL http://127.0.0.1:7101; readonly url.
constructor(url?, options: FlowerAdminOptions = {})Creates the client. Makes no network calls.
FlowerAdminOptionsOptional adminToken, fetch and boundedRetries.
initialize(members, options?): Promise<void>Bootstraps a new cluster from node ID → host:port. Use once, on a fresh group only; never on a restarted member.
deploy(bundle, options?): Promise<DeploymentReceipt>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".
DeploymentOptionsOptional requestId, signal and preparation: "online" | "blocking". Blocking pauses writes during preparation; reads keep working.
DeploymentReceiptrevision, value and duplicate.

admin.partition(name) targets one named database. The other operator methods are documented with their topics: staged deployment, managed keys, partitions and resizing, and retention and transaction closure.

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 or FlowerAdmin 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";
import type pizza from "./pizza.ts";

const transport = createHttp2Transport();
try {
  const db = new FlowerClient<typeof pizza>("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 or compressed responses. Retries come from the client’s retry option.
  • 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.