215 lines · 6.6 KB · TypeScript
RawDownload
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>()(  "BrowserPreviewUnavailableError",  {},) {  override get message(): string {    return "The integrated browser is unavailable in this runtime.";  }} export class BrowserPreviewThreadContextUnavailableError extends Schema.TaggedErrorClass<BrowserPreviewThreadContextUnavailableError>()(  "BrowserPreviewThreadContextUnavailableError",  {},) {  override get message(): string {    return "Thread context is unavailable.";  }} export class BrowserPreviewEnvironmentDisconnectedError extends Schema.TaggedErrorClass<BrowserPreviewEnvironmentDisconnectedError>()(  "BrowserPreviewEnvironmentDisconnectedError",  {    environmentId: Schema.String,    threadId: Schema.String,  },) {  override get message(): string {    return "Environment is not connected.";  }} export class BrowserPreviewAssetUrlInvalidError extends Schema.TaggedErrorClass<BrowserPreviewAssetUrlInvalidError>()(  "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<E = unknown> = (input: {  readonly environmentId: EnvironmentId;  readonly input: PreviewOpenInput;}) => Promise<AtomCommandResult<PreviewSessionSnapshot, E>>; interface PreviewRequest {  readonly isCurrent: () => boolean;  readonly complete: () => void;} const activePreviewRequestByThread = new Map<string, symbol>(); 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 <E>(  input: {    readonly threadRef: ScopedThreadRef;    readonly url: string;    readonly openPreview: OpenPreviewMutation<E>;  },  request: PreviewRequest,): Promise<AtomCommandResult<void, E>> => {  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<E>(input: {  readonly threadRef: ScopedThreadRef;  readonly url: string;  readonly openPreview: OpenPreviewMutation<E>;  readonly signal?: AbortSignal;}): Promise<AtomCommandResult<void, E>> {  const request = beginPreviewRequest(input.threadRef, input.signal);  try {    return await openUrlForPreviewRequest(input, request);  } finally {    request.complete();  }} export async function openFileInPreview<AssetError, PreviewError>(input: {  readonly threadRef: ScopedThreadRef;  readonly filePath: string;  readonly httpBaseUrl: string;  readonly createAssetUrl: (input: {    readonly environmentId: EnvironmentId;    readonly input: { readonly resource: AssetResource };  }) => Promise<AtomCommandResult<AssetCreateUrlResult, AssetError>>;  readonly openPreview: OpenPreviewMutation<PreviewError>;  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();  }}