Skip to main content

๐Ÿ” Transactions

Transactions allow you to atomically modify data across two stores. Either both changes succeed, or neither does.


๐Ÿค” Why Transactions?โ€‹

Consider a trading system where two players exchange items:

-- โŒ Dangerous - if the second update fails, data is inconsistent
Coppermind.updateData(schema, playerA, function(data)
table.remove(data.inventory, itemIndex)
return nil
end)

Coppermind.updateData(schema, playerB, function(data)
table.insert(data.inventory, item) -- What if this fails?
return nil
end)
Data Inconsistency

Without transactions, a failure in the second operation leaves your data in an inconsistent state!

Transactions solve this by ensuring both changes succeed or both are rolled back.

flowchart LR
A["๐Ÿ“ฆ Store A"] --> T["๐Ÿ” Transaction"]
B["๐Ÿ“ฆ Store B"] --> T
T --> C{"โœ… Both succeed?"}
C -->|Yes| D["โœ… Commit"]
C -->|No| E["โ†ฉ๏ธ Rollback"]

๐Ÿš€ Using Transactionsโ€‹

local success, result = Coppermind.transaction(
PlayerSchema,
keyA,
keyB,
function(dataA, dataB)
-- Modify both data tables
local item = table.remove(dataA.inventory, 1)
table.insert(dataB.inventory, item)

dataB.coins -= 100
dataA.coins += 100

return true -- Commit the transaction
end
)

if success then
print("Transaction completed! ID:", result)
else
print("Transaction failed:", result)
end

๐Ÿ“‹ Transaction Callbackโ€‹

The callback receives mutable copies of both stores' data:

function(dataA, dataB)
-- Modify dataA and dataB
dataA.coins += 100
dataB.coins -= 100

return true -- Commit changes
end

โ›” Aborting Transactionsโ€‹

Return false with an optional reason to abort:

local success, result = Coppermind.transaction(
PlayerSchema,
buyerKey,
sellerKey,
function(buyerData, sellerData)
local itemPrice = 500

-- Validate before modifying
if buyerData.coins < itemPrice then
return false, "Buyer has insufficient coins"
end

if #sellerData.inventory == 0 then
return false, "Seller has no items"
end

-- Proceed with trade
buyerData.coins -= itemPrice
sellerData.coins += itemPrice

local item = table.remove(sellerData.inventory, 1)
table.insert(buyerData.inventory, item)

return true
end
)

if not success then
warn("Trade failed:", result)
end

โœ… Transaction Requirementsโ€‹

Both stores must meet these requirements:

RequirementDescription
๐Ÿ“ฆ Same SchemaBoth stores must use the same schema
โœ… Ready StateMust be in READY or SAVING state
๐Ÿ“„ Valid DataBoth stores must have valid data
-- Check stores before transaction
local storeA = Coppermind.getStore(schema, keyA)
local storeB = Coppermind.getStore(schema, keyB)

if not storeA or storeA.state ~= "READY" then
return false, "Player A's data not ready"
end

if not storeB or storeB.state ~= "READY" then
return false, "Player B's data not ready"
end

-- Now safe to transact
Coppermind.transaction(schema, keyA, keyB, callback)

๐Ÿ“ข Transaction Eventsโ€‹

Stores fire onTransaction when a transaction completes:

store.onTransaction:Connect(function(store, transactionId)
print("Completed transaction:", transactionId)
end)

โณ Pending Transactionsโ€‹

Check if a store has pending transactions:

local pending = Coppermind.getPendingTransactionCount(schema, key)

if pending > 0 then
print("Store has", pending, "pending transactions")
end

๐Ÿ’ก Practical Examplesโ€‹

local function trade(
sellerKey: string,
buyerKey: string,
itemId: string,
price: number
): (boolean, string?)
return Coppermind.transaction(
PlayerSchema,
sellerKey,
buyerKey,
function(sellerData, buyerData)
-- Find item in seller's inventory
local itemIndex = table.find(sellerData.inventory, itemId)

if not itemIndex then
return false, "Seller doesn't have item"
end

if buyerData.coins < price then
return false, "Buyer can't afford item"
end

-- Execute trade
table.remove(sellerData.inventory, itemIndex)
table.insert(buyerData.inventory, itemId)

sellerData.coins += price
buyerData.coins -= price

return true
end
)
end

โœ… Best Practicesโ€‹

1. ๐Ÿ›ก๏ธ Validate Earlyโ€‹

Validate First

Check conditions before modifying data to avoid unnecessary work!

function(dataA, dataB)
-- Validate first
if not canTrade(dataA, dataB) then
return false, "Cannot trade"
end

-- Then modify
executeTrade(dataA, dataB)
return true
end

2. โš ๏ธ Handle Failures Gracefullyโ€‹

local success, result = Coppermind.transaction(...)

if not success then
-- Inform players
notifyPlayer(playerA, "Trade failed: " .. result)
notifyPlayer(playerB, "Trade failed: " .. result)
end

3. ๐Ÿ“ Log Transactionsโ€‹

local success, txId = Coppermind.transaction(...)

if success then
print(`[Transaction {txId}] {keyA} <-> {keyB}: Completed`)
else
warn(`[Transaction] {keyA} <-> {keyB}: Failed - {txId}`)
end