@motioneffector/ecs

Documentation

Transactions

Transactions let you make multiple changes atomically. Either all operations succeed, or they all roll back. This is essential when you need to update several entities or components together and can't have partial updates.

How It Works

The ECS wraps your operations in a database transaction. If any operation throws an error, the entire transaction rolls back - the database returns to its state before the transaction started.

Transaction Start
    │
    ├── createEntity()     ✓
    ├── addComponent()     ✓
    ├── updateComponent()  ✗ Error!
    │
    ▼
Rollback: entity and component additions are undone

Basic Usage

await ecs.transaction(async (ecs) => {
  const entity = ecs.createEntity()
  ecs.addComponent(entity, Position, { x: 0, y: 0 })
  ecs.addComponent(entity, Health, { current: 100, max: 100 })
  // If any of these fail, all are rolled back
})

Key Points

  • Automatic rollback - Any error inside the callback triggers a rollback

  • Callback receives ECS - Use the ECS instance passed to the callback for operations

  • Async/await - The transaction method is async, as is the callback

  • Return values - You can return a value from the transaction callback

  • Nested transactions - Supported by the underlying database (SQLite savepoints)

Examples

Atomic Entity Creation

Create an entity that must have all its components or none:

await ecs.transaction(async (ecs) => {
  const player = ecs.createEntity('player')
  ecs.addComponent(player, Position, { x: 0, y: 0 })
  ecs.addComponent(player, Health, { current: 100, max: 100 })
  ecs.addComponent(player, Inventory, { capacity: 20, items: [] })
  // If any addComponent fails, player entity is also rolled back
})

Transferring Resources

Move items between two entities atomically:

await ecs.transaction(async (ecs) => {
  const senderInv = ecs.getComponent(sender, Inventory)
  const receiverInv = ecs.getComponent(receiver, Inventory)

  if (!senderInv || !receiverInv) {
    throw new Error('Missing inventory')
  }

  // Remove from sender
  const newSenderItems = senderInv.items.filter(i => i.id !== itemId)
  ecs.updateComponent(sender, Inventory, { items: newSenderItems })

  // Add to receiver
  const item = senderInv.items.find(i => i.id === itemId)
  const newReceiverItems = [...receiverInv.items, item]
  ecs.updateComponent(receiver, Inventory, { items: newReceiverItems })

  // If either update fails, both are rolled back
})

Returning Values

const newEntityId = await ecs.transaction(async (ecs) => {
  const entity = ecs.createEntity()
  ecs.addComponent(entity, Position, { x: 0, y: 0 })
  return entity  // Return the created ID
})

console.log(`Created: ${newEntityId}`)

Intentional Rollback

await ecs.transaction(async (ecs) => {
  const entity = ecs.createEntity()
  ecs.addComponent(entity, Position, { x: 0, y: 0 })

  // Check some condition
  const entities = ecs.query([Position])
  if (entities.length > 1000) {
    throw new Error('Too many entities')  // Rollback
  }
})

Related