125 lines · 4.2 KB · TypeScript
RawDownload
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));}