Planting checks capacity inside the mutation, so simultaneous gardeners cannot overfill it. Each seed blooms after five seconds and perishes after thirty-five, freeing its spot.
Two private callbacks handle the seasons. Their deadlines and the plant commit together; the planting time protects replacements from old callbacks.
Only plant and view are public. Deadlines mean “not before”; outages can delay both bloom and farewell.
docs/terrarium.ts · continue the same file
type Season = { key: string; plantedAt: number };
const bloom = mutation("internal.bloom", (ctx, event: Season) => {
const flower = ctx.get(flowers, event.key);
if (flower?.plantedAt === event.plantedAt) {
ctx.set(flowers, event.key, { ...flower, bloomed: true });
}
returnnull;
});
const perish = mutation("internal.perish", (ctx, event: Season) => {
if (ctx.get(flowers, event.key)?.plantedAt === event.plantedAt) {
ctx.delete(flowers, event.key);
}
returnnull;
});
const seasons = scheduler("seasons", { bloom, perish });
const plant = mutation("plant", (ctx, seed: Seed) => {
if (!seed || ![seed.garden, seed.id].every(v => typeof v === "string" && v)) {
thrownew Error("Give your garden and seed a name.");
}
const key = canonicalJson([seed.garden, seed.id]);
if (ctx.get(flowers, key)) thrownew Error("That spot is already planted.");
if (ctx.query(flowers.by("garden").eq(seed.garden)).length >= GARDEN_SIZE) {
thrownew Error("Garden full! Wait for a flower to make room.");
}
const plantedAt = ctx.now();
ctx.set(flowers, key, { garden: seed.garden, id: seed.id, plantedAt, bloomed: false });
const event = { key, plantedAt }; // An old timer cannot affect a replacement.
seasons.at(ctx, `bloom:${key}`, plantedAt + 5_000, "bloom", event);
seasons.at(ctx, `perish:${key}`, plantedAt + 35_000, "perish", event);
ctx.materialize(garden, seed.garden);
return { planted: seed.id };
});
exportdefault define({
collections: [flowers, seasons.records],
definitions: [garden, bloom, perish],
maintenance: seasons.maintenance,
http: { "garden.plant": plant, "garden.view": view },
});
03
Share one living view.
Plant with a friend, then watch seeds appear, bloom, and leave empty spots. One SSE watch carries the whole garden; the SDK applies each patch.
This view permits replica lag. Omit replica-local for fresh reads. These gardens are public; add authorization for private worlds.
Watches show current state, not every event. Keep the request ID when retrying a write.
docs/terrarium-client.ts · outside the database
import { FlowerClient } from"@flower-js/sdk";
const terrarium = new FlowerClient("http://127.0.0.1:7101");
await terrarium.mutate("garden.plant", {
garden: "moon-garden", id: "luna",
}, { requestId: "plant-moon-garden-luna" }); // Reuse this ID on retries.forawait (const { value } of terrarium.watch("garden.view", "moon-garden")) {
console.log(value);
// { spacesLeft: 11, blooming: 0, flowers: { luna: "🌱" } }// After 5s: a bloom. After 35s: an empty spot, ready for another seed.
}
Replica-local reads: lag is allowed. 70% reads / 30% mutations · HTTP/2 · 60.3 measured seconds.
Run passed. 8/8 group audits passed. 8 injected leader failures; quorum recovery 592–736 ms.
Apple M5 Pro; all replicas and load generators share one machine. Completed customer calls use the union measurement window; retries, worker traffic, and explicit replays do not inflate throughput. Reads may be stale; mutations and audits retain fresh checks.