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.
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);
}| API | Contract |
|---|---|
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. |
FlowerClientOptions | Optional 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). |
RequestOptions | Optional signal, credentials (overrides the client’s for this call) and retry. |
MutationOptions | Adds optional requestId and expectedRevision. Without a request ID, the call gets a new one, reused across its own retries only. |
RetryPolicy | retry: 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): boolean | True 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): number | A jittered delay between half and all of initialDelayMs · 2^attempt, capped at maxDelayMs. |
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. |
Retrying safely
- A timed-out or lost mutation may still have committed. Retry with the same request ID, arguments and expected revision;
retrydoes 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.
| API | Contract |
|---|---|
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. |
WatchOptions | Optional signal, credentials, maxEventBytes (17 MiB), maxValueBytes (16 MiB) and maxPatchOperations (256). The limits are client-side only. |
SubscribeOptions | WatchOptions 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. |
WatchPollOptions | RequestOptions plus intervalMs (default 250, range 1–2,147,483,647). |
WatchSnapshot<V = Json> | type: "snapshot", sequence, revision, value. |
WatchPatch | type: "patch", sequence, baseSequence, revision, patch. |
WatchDelta<V = Json> | WatchSnapshot or WatchPatch. |
JsonPatchOperation | add/replace with a value, or remove, at a JSON pointer path. An empty path means the whole value. |
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;
monotonichides that. - A redeploy or change of caller identity ends a stream with
WATCH_SCOPE_CHANGED, whichsubscribetreats as a reconnect. Malformed stream data ends it withWATCH_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.
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}` }));| API | Contract |
|---|---|
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. |
FlowerAdminOptions | Optional 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". |
DeploymentOptions | Optional requestId, signal and preparation: "online" | "blocking". Blocking pauses writes during preparation; reads keep working. |
DeploymentReceipt | revision, 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.
| 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 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. |
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
retryoption. - 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.