Files
core2026/app/game-api/src/inputEventBoundary.ts
T
Hide_D 5040691d7c feat: implement input event system with durable command handling
- Introduced InputEvent model with status tracking (PENDING, PROCESSING, SUCCEEDED, FAILED) and unique request IDs.
- Added DatabaseTurnDaemonTransport for sending commands and handling idempotency.
- Implemented executeInputEvent function to manage input event lifecycle and error handling.
- Created DatabaseTurnDaemonCommandQueue for managing command processing and lease recovery.
- Enhanced turn daemon lifecycle to support atomic command execution and error recovery.
- Added tests for input event atomicity, command queuing, and error handling scenarios.
2026-07-25 05:36:34 +00:00

87 lines
2.6 KiB
TypeScript

import type { GamePrisma } from '@sammo-ts/infra';
import type { DatabaseClient } from './context.js';
const asJson = (value: unknown): GamePrisma.InputJsonValue => value as GamePrisma.InputJsonValue;
export class DuplicateInputEventError extends Error {
constructor(readonly requestId: string) {
super(`Input event ${requestId} was already accepted.`);
this.name = 'DuplicateInputEventError';
}
}
export const executeInputEvent = async <T>(options: {
db: DatabaseClient;
requestId: string;
eventType: string;
actorUserId?: string | null;
execute(db: DatabaseClient): Promise<T>;
}): Promise<T> => {
const { db, requestId, eventType, actorUserId, execute } = options;
if (!db.$transaction) {
return execute(db);
}
const processingAt = new Date();
try {
await db.inputEvent.create({
data: {
requestId,
target: 'API',
eventType,
payload: asJson({}),
actorUserId: actorUserId ?? null,
status: 'PROCESSING',
processingAt,
attempts: 1,
},
});
} catch (error) {
const isUniqueConflict =
typeof error === 'object' && error !== null && 'code' in error && error.code === 'P2002';
if (!isUniqueConflict) {
throw error;
}
const claimedRetry = await db.inputEvent.updateMany({
where: { requestId, status: 'FAILED' },
data: {
status: 'PROCESSING',
error: null,
processingAt,
completedAt: null,
attempts: { increment: 1 },
},
});
if (claimedRetry.count === 0) {
throw new DuplicateInputEventError(requestId);
}
}
try {
return await db.$transaction(async (transaction) => {
const result = await execute(transaction);
await transaction.inputEvent.update({
where: { requestId },
data: {
status: 'SUCCEEDED',
result: asJson({ ok: true }),
completedAt: new Date(),
},
});
return result;
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown API input event error.';
await db.inputEvent.update({
where: { requestId },
data: {
status: 'FAILED',
error: message,
completedAt: new Date(),
},
});
throw error;
}
};