Keep a value current
A materialized value is stored and recomputed whenever something it read changes. Say which instances to keep with materialize:
import { aggregate, collection, define, derive, fail, query, v } from "@flower-js/sdk";
const shops = collection("shops", v.object({ name: v.string() }));
const orders = collection("orders", v.object({ shop: v.string(), cents: v.int({ min: 0 }) }))
.index("byShop", ["shop"]);
const revenue = aggregate("shop.revenue", {
source: orders, index: "byShop",
initial: () => 0,
add: (sum, order) => sum + order.cents,
remove: (sum, order) => sum - order.cents,
});
const summary = derive("shop.summary", (ctx, id: string) => ({
...(ctx.get(shops, id) ?? fail("SHOP_NOT_FOUND", `No shop ${id}`)),
revenue: ctx.get(revenue, id),
}), { materialize: { each: shops } });
const shopCount = derive("shop.count", (ctx) => ctx.scan(shops).length, { materialize: "always" });
const read = query("shop.read", { args: v.string() }, (ctx, id) => ctx.get(summary, id));
const count = query("internal.shop.count", (ctx) => ctx.get(shopCount));
const app = define({
definitions: [revenue, summary, shopCount],
http: { "shop.read": read, "shop.count": count },
});
export default app;{ each: shops }keeps one instance per row ofshops, with the row key as its argument. Instances appear and disappear with their rows, in the same commit. Rows that existed before the deploy are picked up in the background."always"keeps the single instance without arguments. It is materialized in the background after the deploy.- Anything else: call
ctx.materialize(value, args)in a mutation, andctx.unmaterializeto stop. - An instance is its name plus its arguments. Omitted arguments mean
null. - Unmaterialized values can still be read; they're computed on the spot.
- A mutation sees its own earlier writes, including through derived values. Record changes, updated values and the result commit together.
The background steps run as maintenance tasks, which the leader checks every 250 ms. Until they finish, reads compute the value on the spot.
Keep dependencies narrow
A value recomputes when anything it read changes, so read only what you need.
- Reading a missing record counts; the value updates when it appears.
ctx.scan(collection)depends on the whole collection. With a prefix or bounds, it depends on that range.- Index lookups depend only on matching entries, as long as the collection is declared: in
define({ collections }), a component, or as the source of an aggregate or trigger. - Split unrelated work into separate derived values, so a tip doesn't recompute every order total.
Maintain totals cheaply
Use aggregate for sums and counts, like revenue above. Give it pure initial, add and remove functions; Flower applies only the changed rows. Read it like any derived value, with the index value as the argument: ctx.get(revenue, "north").
removemust exactly undoadd. Integer sums and counts work well.- The index name and the group type are checked: a two-field index takes a two-element tuple.
- List the aggregate in
definitions. Its source collection is declared for you. - A deploy builds new aggregates in the background. If a write lands meanwhile you get
DEPLOYMENT_CONFLICT: retry with the same request ID, or deploy withpreparation: "blocking". - For large collections, use a staged deployment.
The indexes and aggregates guide has a full example.
How errors behave
- If a derived value throws or calls
fail(), its readers get the same code and message and may catch it. Details aren't kept. - A mutation that throws commits nothing.
- Cycles and exhausted transaction budgets always abort, even if your code catches the error.
- Returning an error-shaped object is a success, not an error.
Callbacks are sandboxed and synchronous. Return JSON. There is no network, filesystem, current date (use ctx.now()) or async work. Globals don't persist between calls. Outside facts come in as method arguments, or from external workers.