Skip to main content
Glama
transaction-manager.ts6.99 kB
import { MODULE_ID } from './constants.js'; export interface TransactionAction { type: 'create' | 'update' | 'delete'; entityType: 'Actor' | 'Token' | 'Scene' | 'Item'; entityId?: string; originalData?: any; newData?: any; rollbackAction?: () => Promise<void>; } export interface Transaction { id: string; timestamp: Date; description: string; actions: TransactionAction[]; completed: boolean; rolledBack: boolean; } export class TransactionManager { private moduleId: string = MODULE_ID; private activeTransactions: Map<string, Transaction> = new Map(); private transactionHistory: Transaction[] = []; /** * Start a new transaction */ startTransaction(description: string): string { const transactionId = foundry.utils.randomID(); const transaction: Transaction = { id: transactionId, timestamp: new Date(), description, actions: [], completed: false, rolledBack: false, }; this.activeTransactions.set(transactionId, transaction); return transactionId; } /** * Add an action to an active transaction */ addAction(transactionId: string, action: TransactionAction): void { const transaction = this.activeTransactions.get(transactionId); if (!transaction) { throw new Error(`Transaction ${transactionId} not found or already completed`); } transaction.actions.push(action); } /** * Commit a transaction (mark as completed) */ commitTransaction(transactionId: string): void { const transaction = this.activeTransactions.get(transactionId); if (!transaction) { throw new Error(`Transaction ${transactionId} not found`); } transaction.completed = true; this.activeTransactions.delete(transactionId); // Add to history (keep last 50 transactions) this.transactionHistory.push(transaction); if (this.transactionHistory.length > 50) { this.transactionHistory.shift(); } } /** * Rollback a transaction (undo all actions) */ async rollbackTransaction(transactionId: string): Promise<{ success: boolean; errors: string[] }> { let transaction = this.activeTransactions.get(transactionId); // Also check completed transactions for rollback if (!transaction) { transaction = this.transactionHistory.find(t => t.id === transactionId); } if (!transaction) { throw new Error(`Transaction ${transactionId} not found`); } if (transaction.rolledBack) { throw new Error(`Transaction ${transactionId} has already been rolled back`); } const errors: string[] = []; // Rollback actions in reverse order for (let i = transaction.actions.length - 1; i >= 0; i--) { const action = transaction.actions[i]; try { await this.rollbackAction(action); } catch (error) { const errorMsg = `Failed to rollback action ${i}: ${error instanceof Error ? error.message : 'Unknown error'}`; errors.push(errorMsg); console.error(`[${this.moduleId}]`, errorMsg); } } transaction.rolledBack = true; // Remove from active transactions if it was there this.activeTransactions.delete(transactionId); const success = errors.length === 0; return { success, errors }; } /** * Rollback a specific action */ private async rollbackAction(action: TransactionAction): Promise<void> { switch (action.type) { case 'create': await this.rollbackCreate(action); break; case 'update': await this.rollbackUpdate(action); break; case 'delete': await this.rollbackDelete(action); break; default: throw new Error(`Unknown action type: ${action.type}`); } } /** * Rollback a create action (delete the created entity) */ private async rollbackCreate(action: TransactionAction): Promise<void> { if (!action.entityId) { throw new Error('Cannot rollback create action: missing entityId'); } switch (action.entityType) { case 'Actor': const actor = game.actors.get(action.entityId); if (actor) { await actor.delete(); } break; case 'Token': // Find token in current scene const scene = (game.scenes as any).current; if (scene) { const token = scene.tokens.get(action.entityId); if (token) { await token.delete(); } } break; default: throw new Error(`Rollback not implemented for entity type: ${action.entityType}`); } } /** * Rollback an update action (restore original data) */ private async rollbackUpdate(action: TransactionAction): Promise<void> { if (!action.entityId || !action.originalData) { throw new Error('Cannot rollback update action: missing entityId or originalData'); } switch (action.entityType) { case 'Actor': const actor = game.actors.get(action.entityId); if (actor) { await actor.update(action.originalData); } break; default: throw new Error(`Rollback not implemented for entity type: ${action.entityType}`); } } /** * Rollback a delete action (recreate the entity) */ private async rollbackDelete(action: TransactionAction): Promise<void> { if (!action.originalData) { throw new Error('Cannot rollback delete action: missing originalData'); } switch (action.entityType) { case 'Actor': await Actor.create(action.originalData); break; default: throw new Error(`Rollback not implemented for entity type: ${action.entityType}`); } } /** * Get active transactions */ getActiveTransactions(): Transaction[] { return Array.from(this.activeTransactions.values()); } /** * Get transaction history */ getTransactionHistory(): Transaction[] { return [...this.transactionHistory]; } /** * Clear old transactions from history */ clearHistory(): void { this.transactionHistory = []; } /** * Cancel an active transaction without rollback (use for cleanup) */ cancelTransaction(transactionId: string): void { const transaction = this.activeTransactions.get(transactionId); if (transaction) { this.activeTransactions.delete(transactionId); } } /** * Create rollback action for actor creation */ createActorCreationAction(actorId: string): TransactionAction { return { type: 'create', entityType: 'Actor', entityId: actorId, }; } /** * Create rollback action for token creation */ createTokenCreationAction(tokenId: string): TransactionAction { return { type: 'create', entityType: 'Token', entityId: tokenId, }; } } // Export singleton instance export const transactionManager = new TransactionManager();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adambdooley/foundry-vtt-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server