@motioneffector/flags

Documentation

Batch Operations API

Group multiple changes into a single atomic operation.


batch()

Executes a function with batched updates. All changes are grouped and subscribers are notified once at the end.

Signature:

batch<T>(fn: () => T): T

Parameters:

Name Type Required Description
fn () => T Yes Function to execute within the batch

Returns: T — The return value of the function.

Throws:

  • Re-throws any error from the function (after rolling back changes)

Example:

// Without batch: 3 notifications
store.set('a', 1)
store.set('b', 2)
store.set('c', 3)

// With batch: 1 notification
store.batch(() => {
  store.set('x', 1)
  store.set('y', 2)
  store.set('z', 3)
})

Behavior Details

Single Notification

Subscribers are notified once after the batch completes:

let notifyCount = 0
store.subscribe(() => notifyCount++)

store.batch(() => {
  store.set('a', 1)
  store.set('b', 2)
  store.set('c', 3)
})

console.log(notifyCount)  // 1

Return Value

The batch returns whatever your function returns:

const result = store.batch(() => {
  store.set('gold', 100)
  return store.get('gold')
})

console.log(result)  // 100

Error Rollback

If the function throws, all changes are rolled back:

store.set('count', 0)

try {
  store.batch(() => {
    store.set('count', 100)
    store.set('name', 'test')
    throw new Error('Oops')
  })
} catch (e) {
  // Error propagates
}

store.get('count')   // 0 (rolled back)
store.has('name')    // false (rolled back)

No Notifications on Error

If a batch errors, no notifications are fired:

let notified = false
store.subscribe(() => notified = true)

try {
  store.batch(() => {
    store.set('x', 1)
    throw new Error('Oops')
  })
} catch (e) {}

console.log(notified)  // false

Nested Batches

Nested batches are flattened into the outermost batch:

let notifyCount = 0
store.subscribe(() => notifyCount++)

store.batch(() => {
  store.set('a', 1)

  store.batch(() => {
    store.set('b', 2)
  })  // No notification here

  store.set('c', 3)
})  // Single notification here

console.log(notifyCount)  // 1

Single History Step

With history enabled, a batch creates a single undo step:

const store = createFlagStore({ history: true })

store.batch(() => {
  store.set('a', 1)
  store.set('b', 2)
  store.set('c', 3)
})

store.undo()  // Reverts ALL three changes

store.get('a')  // undefined
store.get('b')  // undefined
store.get('c')  // undefined

Key Subscriptions

Key-specific subscriptions fire once per affected key after the batch:

store.subscribeKey('a', () => console.log('a changed'))
store.subscribeKey('b', () => console.log('b changed'))
store.subscribeKey('c', () => console.log('c changed'))

store.batch(() => {
  store.set('a', 1)
  store.set('b', 2)
})

// After batch:
// Logs: "a changed"
// Logs: "b changed"
// (c not logged - wasn't changed)

All Operations Work

Any store operation works inside a batch:

store.batch(() => {
  store.set('flag', true)
  store.toggle('flag')
  store.set('count', 0)
  store.increment('count', 10)
  store.decrement('count', 3)
  store.setMany({ x: 1, y: 2 })
  store.delete('temp')
})

With Persistence

Batched changes are saved once at the end:

const store = createFlagStore({
  persist: { storage: localStorage }
})

store.batch(() => {
  store.set('a', 1)
  store.set('b', 2)
  store.set('c', 3)
})

// Single save to localStorage after batch

Changes Visible Inside Batch

Changes are immediately visible within the batch:

store.batch(() => {
  store.set('x', 10)
  console.log(store.get('x'))  // 10 (visible immediately)

  store.increment('x', 5)
  console.log(store.get('x'))  // 15
})

Use Cases

Atomic Updates

Ensure related changes happen together:

store.batch(() => {
  store.decrement('player_health', damage)
  store.increment('enemy_score', damage)
  store.set('last_hit_time', Date.now())
})

Performance Optimization

Prevent multiple re-renders:

store.batch(() => {
  // Update many values without triggering re-render for each
  for (const [key, value] of Object.entries(newState)) {
    store.set(key, value)
  }
})
// Single re-render after all updates

Transaction-Like Behavior

All-or-nothing updates:

function transferGold(from: string, to: string, amount: number) {
  store.batch(() => {
    const fromGold = store.get(`${from}.gold`) as number
    if (fromGold < amount) {
      throw new Error('Insufficient gold')
    }
    store.decrement(`${from}.gold`, amount)
    store.increment(`${to}.gold`, amount)
  })
}