Skip to content

Transactions

A transaction is an explicit economic action: preview requirements, pay a cost, apply the game effect and credit a reward.

ts
import { createEconomy, describeFailure, type Transaction } from "@idlekitjs/economy";

Shape

ts
interface Transaction<T> {
  id: string;
  label?: string;
  requirements?: readonly Requirement<T>[];
  cost?: AmountsInput | ((state: T) => AmountsInput);
  reward?: AmountsInput | ((state: T) => AmountsInput);
  apply?: (state: T) => void;
  metadata?: Record<string, unknown>;
}

Costs and rewards can be static amount inputs or functions of the current state. Function forms are resolved once per preview.

Preview

economy.preview(state, transaction) is pure. It does not mutate state and it collects every failure it can report.

ts
const preview = economy.preview(state, {
  id: "collect:coins",
  reward: [["currency:coins", 25]],
});

if (preview.ok) {
  // preview.cost, preview.reward, preview.overflow
} else {
  // preview.failures, preview.missing
}

Preview resolves and normalizes cost and reward lines, checks unknown resources, invalid amounts, read-only resources, requirements, affordability and reward overflow.

Execute

economy.execute(state, transaction) runs the same preview first. If preview fails, it returns { ok: false, failures, missing } and mutates nothing.

If preview succeeds, execution mutates in this order:

  1. pay the normalized cost;
  2. run apply(state);
  3. credit the normalized reward.
ts
const result = economy.execute(state, {
  id: "upgrade:stronger-clicks",
  cost: [["currency:coins", 100]],
  reward: [["currency:xp", 10]],
  apply: (state) => {
    state.upgrades.push("stronger-clicks");
  },
});

if (!result.ok) {
  console.warn(result.failures.map((failure) => describeFailure(failure)).join("\n"));
}

Rewards are credited only after successful execution. A failed transaction does not grant rewards.

Apply contract

There is no generic rollback. apply runs after the cost is paid and before the reward is credited. It must be safe after preview.

If apply can fail, express the blocking condition as a requirement instead. If apply throws, the exception propagates as a programming error and the already-paid cost is not rolled back.

Collect example

Pickups create temporary items. Collection can be settled by an Economy transaction so rewards are granted only on success.

ts
import { collectPickup } from "@idlekitjs/mechanics/pickups/economy";

const result = economy.execute(
  state,
  collectPickup(pickupsExt, {
    itemId: "coin#4",
    reward: [["currency:coins", 25]],
  }),
);

If the pickup is gone or expired, the transaction fails and credits nothing.