import type { OrchestrationCommand, OrchestrationProject, OrchestrationThread, ProjectId, ThreadId,} from "@t3tools/contracts";import { normalizeProjectPathForComparison } from "@t3tools/shared/path";import * as Effect from "effect/Effect";import * as HashMap from "effect/HashMap"; import { findProjectById, findThreadById, isThreadDeleted, listThreadsByProjectId, type CommandReadModel,} from "./commandReadModel.ts";import { OrchestrationCommandInvariantError } from "./Errors.ts"; export { findProjectById, findThreadById, listThreadsByProjectId }; function invariantError(commandType: string, detail: string): OrchestrationCommandInvariantError { return new OrchestrationCommandInvariantError({ commandType, detail, });} export function requireProject(input: { readonly readModel: CommandReadModel; readonly command: OrchestrationCommand; readonly projectId: ProjectId;}): Effect.Effect<OrchestrationProject, OrchestrationCommandInvariantError> { const project = findProjectById(input.readModel, input.projectId); if (project) { return Effect.succeed(project); } return Effect.fail( invariantError( input.command.type, `Project '${input.projectId}' does not exist for command '${input.command.type}'.`, ), );} export function requireProjectAbsent(input: { readonly readModel: CommandReadModel; readonly command: OrchestrationCommand; readonly projectId: ProjectId;}): Effect.Effect<void, OrchestrationCommandInvariantError> { if (!findProjectById(input.readModel, input.projectId)) { return Effect.void; } return Effect.fail( invariantError( input.command.type, `Project '${input.projectId}' already exists and cannot be created twice.`, ), );} export function requireActiveProjectWorkspaceRootAbsent(input: { readonly readModel: CommandReadModel; readonly command: OrchestrationCommand; readonly workspaceRoot: string; readonly exceptProjectId?: ProjectId;}): Effect.Effect<void, OrchestrationCommandInvariantError> { const normalizedWorkspaceRoot = normalizeProjectPathForComparison(input.workspaceRoot); let existingProject: OrchestrationProject | undefined; for (const project of HashMap.values(input.readModel.projects)) { if ( project.deletedAt === null && normalizeProjectPathForComparison(project.workspaceRoot) === normalizedWorkspaceRoot && project.id !== input.exceptProjectId ) { existingProject = project; break; } } if (existingProject === undefined) { return Effect.void; } return Effect.fail( invariantError( input.command.type, `Active project '${existingProject.id}' already exists for workspace root '${normalizedWorkspaceRoot}'.`, ), );} export function requireThread(input: { readonly readModel: CommandReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId;}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> { const thread = findThreadById(input.readModel, input.threadId); if (thread) { return Effect.succeed(thread); } return Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' does not exist for command '${input.command.type}'.`, ), );} export function requireThreadArchived(input: { readonly readModel: CommandReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId;}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> { return requireThread(input).pipe( Effect.flatMap((thread) => thread.archivedAt !== null ? Effect.succeed(thread) : Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' is not archived for command '${input.command.type}'.`, ), ), ), );} export function requireThreadNotArchived(input: { readonly readModel: CommandReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId;}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> { return requireThread(input).pipe( Effect.flatMap((thread) => thread.archivedAt === null ? Effect.succeed(thread) : Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' is already archived and cannot handle command '${input.command.type}'.`, ), ), ), );} export function requireThreadAbsent(input: { readonly readModel: CommandReadModel; readonly command: OrchestrationCommand; readonly threadId: ThreadId;}): Effect.Effect<void, OrchestrationCommandInvariantError> { // A deleted thread is evicted from `threads` but its id is retained in // `deletedThreadIds`, so reject re-using a live OR previously-deleted id. if ( !findThreadById(input.readModel, input.threadId) && !isThreadDeleted(input.readModel, input.threadId) ) { return Effect.void; } return Effect.fail( invariantError( input.command.type, `Thread '${input.threadId}' already exists and cannot be created twice.`, ), );} export function requireNonNegativeInteger(input: { readonly commandType: OrchestrationCommand["type"]; readonly field: string; readonly value: number;}): Effect.Effect<void, OrchestrationCommandInvariantError> { if (Number.isInteger(input.value) && input.value >= 0) { return Effect.void; } return Effect.fail( invariantError( input.commandType, `${input.field} must be an integer greater than or equal to 0.`, ), );}
import type {
OrchestrationCommand,
OrchestrationProject,
OrchestrationThread,
ProjectId,
ThreadId,
} from "@t3tools/contracts";
import { normalizeProjectPathForComparison } from "@t3tools/shared/path";
import * as Effect from "effect/Effect";
import * as HashMap from "effect/HashMap";
import {
findProjectById,
findThreadById,
isThreadDeleted,
listThreadsByProjectId,
type CommandReadModel,
} from "./commandReadModel.ts";
import { OrchestrationCommandInvariantError } from "./Errors.ts";
export { findProjectById, findThreadById, listThreadsByProjectId };
function invariantError(commandType: string, detail: string): OrchestrationCommandInvariantError {
return new OrchestrationCommandInvariantError({
commandType,
detail,
});
}
export function requireProject(input: {
readonly readModel: CommandReadModel;
readonly command: OrchestrationCommand;
readonly projectId: ProjectId;
}): Effect.Effect<OrchestrationProject, OrchestrationCommandInvariantError> {
const project = findProjectById(input.readModel, input.projectId);
if (project) {
return Effect.succeed(project);
}
return Effect.fail(
invariantError(
input.command.type,
`Project '${input.projectId}' does not exist for command '${input.command.type}'.`,
),
);
}
export function requireProjectAbsent(input: {
readonly readModel: CommandReadModel;
readonly command: OrchestrationCommand;
readonly projectId: ProjectId;
}): Effect.Effect<void, OrchestrationCommandInvariantError> {
if (!findProjectById(input.readModel, input.projectId)) {
return Effect.void;
}
return Effect.fail(
invariantError(
input.command.type,
`Project '${input.projectId}' already exists and cannot be created twice.`,
),
);
}
export function requireActiveProjectWorkspaceRootAbsent(input: {
readonly readModel: CommandReadModel;
readonly command: OrchestrationCommand;
readonly workspaceRoot: string;
readonly exceptProjectId?: ProjectId;
}): Effect.Effect<void, OrchestrationCommandInvariantError> {
const normalizedWorkspaceRoot = normalizeProjectPathForComparison(input.workspaceRoot);
let existingProject: OrchestrationProject | undefined;
for (const project of HashMap.values(input.readModel.projects)) {
if (
project.deletedAt === null &&
normalizeProjectPathForComparison(project.workspaceRoot) === normalizedWorkspaceRoot &&
project.id !== input.exceptProjectId
) {
existingProject = project;
break;
}
}
if (existingProject === undefined) {
return Effect.void;
}
return Effect.fail(
invariantError(
input.command.type,
`Active project '${existingProject.id}' already exists for workspace root '${normalizedWorkspaceRoot}'.`,
),
);
}
export function requireThread(input: {
readonly readModel: CommandReadModel;
readonly command: OrchestrationCommand;
readonly threadId: ThreadId;
}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> {
const thread = findThreadById(input.readModel, input.threadId);
if (thread) {
return Effect.succeed(thread);
}
return Effect.fail(
invariantError(
input.command.type,
`Thread '${input.threadId}' does not exist for command '${input.command.type}'.`,
),
);
}
export function requireThreadArchived(input: {
readonly readModel: CommandReadModel;
readonly command: OrchestrationCommand;
readonly threadId: ThreadId;
}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> {
return requireThread(input).pipe(
Effect.flatMap((thread) =>
thread.archivedAt !== null
? Effect.succeed(thread)
: Effect.fail(
invariantError(
input.command.type,
`Thread '${input.threadId}' is not archived for command '${input.command.type}'.`,
),
),
),
);
}
export function requireThreadNotArchived(input: {
readonly readModel: CommandReadModel;
readonly command: OrchestrationCommand;
readonly threadId: ThreadId;
}): Effect.Effect<OrchestrationThread, OrchestrationCommandInvariantError> {
return requireThread(input).pipe(
Effect.flatMap((thread) =>
thread.archivedAt === null
? Effect.succeed(thread)
: Effect.fail(
invariantError(
input.command.type,
`Thread '${input.threadId}' is already archived and cannot handle command '${input.command.type}'.`,
),
),
),
);
}
export function requireThreadAbsent(input: {
readonly readModel: CommandReadModel;
readonly command: OrchestrationCommand;
readonly threadId: ThreadId;
}): Effect.Effect<void, OrchestrationCommandInvariantError> {
// A deleted thread is evicted from `threads` but its id is retained in
// `deletedThreadIds`, so reject re-using a live OR previously-deleted id.
if (
!findThreadById(input.readModel, input.threadId) &&
!isThreadDeleted(input.readModel, input.threadId)
) {
return Effect.void;
}
return Effect.fail(
invariantError(
input.command.type,
`Thread '${input.threadId}' already exists and cannot be created twice.`,
),
);
}
export function requireNonNegativeInteger(input: {
readonly commandType: OrchestrationCommand["type"];
readonly field: string;
readonly value: number;
}): Effect.Effect<void, OrchestrationCommandInvariantError> {
if (Number.isInteger(input.value) && input.value >= 0) {
return Effect.void;
}
return Effect.fail(
invariantError(
input.commandType,
`${input.field} must be an integer greater than or equal to 0.`,
),
);
}