import { isTransportConnectionErrorMessage } from "@t3tools/client-runtime/errors";import { isEnvironmentRpcUnavailableError } from "@t3tools/client-runtime/rpc";import type { EnvironmentShellStatus } from "@t3tools/client-runtime/state/shell";import { CommandId, EnvironmentId, IsoDateTime, MessageId, ThreadId } from "@t3tools/contracts";import * as Schema from "effect/Schema"; import type { DraftComposerImageAttachment } from "../lib/composerImages";import { scopedThreadKey } from "../lib/scopedEntities"; const THREAD_OUTBOX_SCHEMA_VERSION = 1;const THREAD_OUTBOX_MAX_RETRY_DELAY_MS = 16_000; const DraftComposerImageAttachmentSchema = Schema.Struct({ id: Schema.String, previewUri: Schema.String, type: Schema.Literal("image"), name: Schema.String, mimeType: Schema.String, sizeBytes: Schema.Number, dataUrl: Schema.String,}); export const QueuedThreadMessageSchema = Schema.Struct({ schemaVersion: Schema.Literal(THREAD_OUTBOX_SCHEMA_VERSION), environmentId: EnvironmentId, threadId: ThreadId, messageId: MessageId, commandId: CommandId, text: Schema.String, attachments: Schema.Array(DraftComposerImageAttachmentSchema), createdAt: IsoDateTime,}); const decodeStoredQueuedThreadMessage = Schema.decodeUnknownSync(QueuedThreadMessageSchema);const encodeStoredQueuedThreadMessage = Schema.encodeUnknownSync(QueuedThreadMessageSchema); export interface QueuedThreadMessage { readonly environmentId: EnvironmentId; readonly threadId: ThreadId; readonly messageId: MessageId; readonly commandId: CommandId; readonly text: string; readonly attachments: ReadonlyArray<DraftComposerImageAttachment>; readonly createdAt: string;} export function encodeQueuedThreadMessage(message: QueuedThreadMessage): unknown { return encodeStoredQueuedThreadMessage({ schemaVersion: THREAD_OUTBOX_SCHEMA_VERSION, ...message, });} export function decodeQueuedThreadMessage(value: unknown): QueuedThreadMessage { const { schemaVersion: _, ...message } = decodeStoredQueuedThreadMessage(value); return message;} export function groupQueuedThreadMessages( messages: ReadonlyArray<QueuedThreadMessage>,): Record<string, ReadonlyArray<QueuedThreadMessage>> { const deduplicated = new Map<MessageId, QueuedThreadMessage>(); for (const message of messages) { deduplicated.set(message.messageId, message); } const grouped: Record<string, Array<QueuedThreadMessage>> = {}; for (const message of deduplicated.values()) { const threadKey = scopedThreadKey(message.environmentId, message.threadId); (grouped[threadKey] ??= []).push(message); } for (const queue of Object.values(grouped)) { queue.sort((left, right) => left.createdAt.localeCompare(right.createdAt)); } return grouped;} export function flattenQueuedThreadMessages( queues: Record<string, ReadonlyArray<QueuedThreadMessage>>,): ReadonlyArray<QueuedThreadMessage> { return Object.values(queues).flat();} export function threadOutboxRetryDelayMs(attempt: number): number { return Math.min(1_000 * 2 ** Math.max(0, attempt - 1), THREAD_OUTBOX_MAX_RETRY_DELAY_MS);} export type ThreadOutboxDeliveryAction = "wait" | "remove" | "send"; export function resolveThreadOutboxDeliveryAction(input: { readonly threadExists: boolean; readonly shellStatus: EnvironmentShellStatus; readonly environmentConnected: boolean; readonly threadBusy: boolean;}): ThreadOutboxDeliveryAction { if (!input.threadExists) { return input.shellStatus === "live" ? "remove" : "wait"; } return input.environmentConnected && !input.threadBusy ? "send" : "wait";} function errorMessage(error: unknown): string | null { if (error instanceof Error) { return error.message; } if (typeof error === "object" && error !== null && "message" in error) { return typeof error.message === "string" ? error.message : null; } return typeof error === "string" ? error : null;} export function shouldRetryThreadOutboxDelivery(error: unknown): boolean { if (isEnvironmentRpcUnavailableError(error)) { return true; } if ( typeof error === "object" && error !== null && "_tag" in error && error._tag === "ConnectionTransientError" ) { return true; } return isTransportConnectionErrorMessage(errorMessage(error));}
import { isTransportConnectionErrorMessage } from "@t3tools/client-runtime/errors";
import { isEnvironmentRpcUnavailableError } from "@t3tools/client-runtime/rpc";
import type { EnvironmentShellStatus } from "@t3tools/client-runtime/state/shell";
import { CommandId, EnvironmentId, IsoDateTime, MessageId, ThreadId } from "@t3tools/contracts";
import * as Schema from "effect/Schema";
import type { DraftComposerImageAttachment } from "../lib/composerImages";
import { scopedThreadKey } from "../lib/scopedEntities";
const THREAD_OUTBOX_SCHEMA_VERSION = 1;
const THREAD_OUTBOX_MAX_RETRY_DELAY_MS = 16_000;
const DraftComposerImageAttachmentSchema = Schema.Struct({
id: Schema.String,
previewUri: Schema.String,
type: Schema.Literal("image"),
name: Schema.String,
mimeType: Schema.String,
sizeBytes: Schema.Number,
dataUrl: Schema.String,
});
export const QueuedThreadMessageSchema = Schema.Struct({
schemaVersion: Schema.Literal(THREAD_OUTBOX_SCHEMA_VERSION),
environmentId: EnvironmentId,
threadId: ThreadId,
messageId: MessageId,
commandId: CommandId,
text: Schema.String,
attachments: Schema.Array(DraftComposerImageAttachmentSchema),
createdAt: IsoDateTime,
});
const decodeStoredQueuedThreadMessage = Schema.decodeUnknownSync(QueuedThreadMessageSchema);
const encodeStoredQueuedThreadMessage = Schema.encodeUnknownSync(QueuedThreadMessageSchema);
export interface QueuedThreadMessage {
readonly environmentId: EnvironmentId;
readonly threadId: ThreadId;
readonly messageId: MessageId;
readonly commandId: CommandId;
readonly text: string;
readonly attachments: ReadonlyArray<DraftComposerImageAttachment>;
readonly createdAt: string;
}
export function encodeQueuedThreadMessage(message: QueuedThreadMessage): unknown {
return encodeStoredQueuedThreadMessage({
schemaVersion: THREAD_OUTBOX_SCHEMA_VERSION,
...message,
});
}
export function decodeQueuedThreadMessage(value: unknown): QueuedThreadMessage {
const { schemaVersion: _, ...message } = decodeStoredQueuedThreadMessage(value);
return message;
}
export function groupQueuedThreadMessages(
messages: ReadonlyArray<QueuedThreadMessage>,
): Record<string, ReadonlyArray<QueuedThreadMessage>> {
const deduplicated = new Map<MessageId, QueuedThreadMessage>();
for (const message of messages) {
deduplicated.set(message.messageId, message);
}
const grouped: Record<string, Array<QueuedThreadMessage>> = {};
for (const message of deduplicated.values()) {
const threadKey = scopedThreadKey(message.environmentId, message.threadId);
(grouped[threadKey] ??= []).push(message);
}
for (const queue of Object.values(grouped)) {
queue.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
}
return grouped;
}
export function flattenQueuedThreadMessages(
queues: Record<string, ReadonlyArray<QueuedThreadMessage>>,
): ReadonlyArray<QueuedThreadMessage> {
return Object.values(queues).flat();
}
export function threadOutboxRetryDelayMs(attempt: number): number {
return Math.min(1_000 * 2 ** Math.max(0, attempt - 1), THREAD_OUTBOX_MAX_RETRY_DELAY_MS);
}
export type ThreadOutboxDeliveryAction = "wait" | "remove" | "send";
export function resolveThreadOutboxDeliveryAction(input: {
readonly threadExists: boolean;
readonly shellStatus: EnvironmentShellStatus;
readonly environmentConnected: boolean;
readonly threadBusy: boolean;
}): ThreadOutboxDeliveryAction {
if (!input.threadExists) {
return input.shellStatus === "live" ? "remove" : "wait";
}
return input.environmentConnected && !input.threadBusy ? "send" : "wait";
}
function errorMessage(error: unknown): string | null {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "object" && error !== null && "message" in error) {
return typeof error.message === "string" ? error.message : null;
}
return typeof error === "string" ? error : null;
}
export function shouldRetryThreadOutboxDelivery(error: unknown): boolean {
if (isEnvironmentRpcUnavailableError(error)) {
return true;
}
if (
typeof error === "object" &&
error !== null &&
"_tag" in error &&
error._tag === "ConnectionTransientError"
) {
return true;
}
return isTransportConnectionErrorMessage(errorMessage(error));
}