import type { AssetCreateUrlResult, AssetResource, EnvironmentId, PreviewOpenInput, PreviewSessionSnapshot, ScopedThreadRef, } from "@t3tools/contracts"; import { scopedThreadKey } from "@t3tools/client-runtime/environment"; import { type AtomCommandResult, mapAtomCommandResult, } from "@t3tools/client-runtime/state/runtime"; import { isWorkspaceBrowserPreviewPath, isWorkspaceImagePreviewPath, isWorkspacePreviewEntryPath, } from "@t3tools/shared/filePreview"; import * as Cause from "effect/Cause"; import * as Schema from "effect/Schema"; import { AsyncResult } from "effect/unstable/reactivity"; import { applyPreviewServerSnapshot, isPreviewSupportedInRuntime, rememberPreviewUrl, } from "~/previewStateStore"; import { useRightPanelStore } from "~/rightPanelStore"; export const isBrowserPreviewFile = isWorkspaceBrowserPreviewPath; export const isImagePreviewFile = isWorkspaceImagePreviewPath; export const isWorkspacePreviewFile = isWorkspacePreviewEntryPath; export class BrowserPreviewUnavailableError extends Schema.TaggedErrorClass()( "BrowserPreviewUnavailableError", {}, ) { override get message(): string { return "The integrated browser is unavailable in this runtime."; } } export class BrowserPreviewThreadContextUnavailableError extends Schema.TaggedErrorClass()( "BrowserPreviewThreadContextUnavailableError", {}, ) { override get message(): string { return "Thread context is unavailable."; } } export class BrowserPreviewEnvironmentDisconnectedError extends Schema.TaggedErrorClass()( "BrowserPreviewEnvironmentDisconnectedError", { environmentId: Schema.String, threadId: Schema.String, }, ) { override get message(): string { return "Environment is not connected."; } } export class BrowserPreviewAssetUrlInvalidError extends Schema.TaggedErrorClass()( "BrowserPreviewAssetUrlInvalidError", { environmentId: Schema.String, threadId: Schema.String, filePath: Schema.String, httpBaseUrlLength: Schema.Int, relativeUrlLength: Schema.Int, expiresAt: Schema.Int, cause: Schema.Defect(), }, ) { override get message(): string { return "The environment returned an invalid asset URL."; } } export const isBrowserPreviewAssetUrlInvalidError = Schema.is(BrowserPreviewAssetUrlInvalidError); export type OpenPreviewMutation = (input: { readonly environmentId: EnvironmentId; readonly input: PreviewOpenInput; }) => Promise>; interface PreviewRequest { readonly isCurrent: () => boolean; readonly complete: () => void; } const activePreviewRequestByThread = new Map(); const beginPreviewRequest = ( threadRef: ScopedThreadRef, signal: AbortSignal | undefined, ): PreviewRequest => { const threadKey = scopedThreadKey(threadRef); const requestId = Symbol(); activePreviewRequestByThread.set(threadKey, requestId); return { isCurrent: () => activePreviewRequestByThread.get(threadKey) === requestId && signal?.aborted !== true, complete: () => { if (activePreviewRequestByThread.get(threadKey) === requestId) { activePreviewRequestByThread.delete(threadKey); } }, }; }; const openUrlForPreviewRequest = async ( input: { readonly threadRef: ScopedThreadRef; readonly url: string; readonly openPreview: OpenPreviewMutation; }, request: PreviewRequest, ): Promise> => { const result = await input.openPreview({ environmentId: input.threadRef.environmentId, input: { threadId: input.threadRef.threadId, url: input.url }, }); if (!request.isCurrent()) { return AsyncResult.success(undefined); } return mapAtomCommandResult(result, (snapshot) => { applyPreviewServerSnapshot(input.threadRef, snapshot); rememberPreviewUrl(input.threadRef, input.url); useRightPanelStore.getState().openBrowser(input.threadRef, snapshot.tabId); }); }; export async function openUrlInPreview(input: { readonly threadRef: ScopedThreadRef; readonly url: string; readonly openPreview: OpenPreviewMutation; readonly signal?: AbortSignal; }): Promise> { const request = beginPreviewRequest(input.threadRef, input.signal); try { return await openUrlForPreviewRequest(input, request); } finally { request.complete(); } } export async function openFileInPreview(input: { readonly threadRef: ScopedThreadRef; readonly filePath: string; readonly httpBaseUrl: string; readonly createAssetUrl: (input: { readonly environmentId: EnvironmentId; readonly input: { readonly resource: AssetResource }; }) => Promise>; readonly openPreview: OpenPreviewMutation; readonly signal?: AbortSignal; }): Promise< AtomCommandResult< void, AssetError | PreviewError | BrowserPreviewUnavailableError | BrowserPreviewAssetUrlInvalidError > > { const request = beginPreviewRequest(input.threadRef, input.signal); try { if (!isPreviewSupportedInRuntime()) { return AsyncResult.failure(Cause.fail(new BrowserPreviewUnavailableError())); } const assetResult = await input.createAssetUrl({ environmentId: input.threadRef.environmentId, input: { resource: { _tag: "workspace-file", threadId: input.threadRef.threadId, path: input.filePath, }, }, }); if (!request.isCurrent()) { return AsyncResult.success(undefined); } if (assetResult._tag === "Failure") { return AsyncResult.failure(assetResult.cause); } let assetUrl: string; try { assetUrl = new URL(assetResult.value.relativeUrl, input.httpBaseUrl).toString(); } catch (cause) { return AsyncResult.failure( Cause.fail( new BrowserPreviewAssetUrlInvalidError({ environmentId: input.threadRef.environmentId, threadId: input.threadRef.threadId, filePath: input.filePath, httpBaseUrlLength: input.httpBaseUrl.length, relativeUrlLength: assetResult.value.relativeUrl.length, expiresAt: assetResult.value.expiresAt, cause, }), ), ); } return await openUrlForPreviewRequest( { threadRef: input.threadRef, url: assetUrl, openPreview: input.openPreview, }, request, ); } finally { request.complete(); } }