diff --git a/apps/web/src/browser/previewWebviewConfigState.test.ts b/apps/web/src/browser/previewWebviewConfigState.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..35eb665eb7e37e73d3e47bd9ce19125d9ac92dbe --- /dev/null +++ b/apps/web/src/browser/previewWebviewConfigState.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "@effect/vitest"; +import { EnvironmentId } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; + +import { + loadPreviewWebviewConfig, + PreviewWebviewBridgeUnavailableError, + PreviewWebviewConfigLoadError, +} from "./previewWebviewConfigState"; + +const environmentId = EnvironmentId.make("environment-1"); + +describe("loadPreviewWebviewConfig", () => { + it.effect("reports a structurally distinct missing-bridge failure", () => + Effect.gen(function* () { + const error = yield* loadPreviewWebviewConfig(environmentId, null).pipe(Effect.flip); + + expect(error).toBeInstanceOf(PreviewWebviewBridgeUnavailableError); + expect(error.environmentId).toBe(environmentId); + expect(error.message).toContain(environmentId); + expect("cause" in error).toBe(false); + }), + ); + + it.effect("preserves the bridge rejection as the load failure cause", () => + Effect.gen(function* () { + const cause = new Error("ipc unavailable"); + const error = yield* loadPreviewWebviewConfig(environmentId, { + getPreviewConfig: () => Promise.reject(cause), + }).pipe(Effect.flip); + + expect(error).toBeInstanceOf(PreviewWebviewConfigLoadError); + expect(error.environmentId).toBe(environmentId); + expect(error.cause).toBe(cause); + expect(error.message).not.toContain(cause.message); + }), + ); + + it.effect("forwards the environment id to the bridge", () => + Effect.gen(function* () { + let requestedEnvironmentId: EnvironmentId | null = null; + const config = { + partition: "persist:test-preview", + webPreferences: "sandbox=yes", + preloadUrl: null, + }; + const result = yield* loadPreviewWebviewConfig(environmentId, { + getPreviewConfig: (input) => { + requestedEnvironmentId = input; + return Promise.resolve(config); + }, + }); + + expect(requestedEnvironmentId).toBe(environmentId); + expect(result).toEqual(config); + }), + ); +}); diff --git a/apps/web/src/browser/previewWebviewConfigState.ts b/apps/web/src/browser/previewWebviewConfigState.ts index 99a8388ec5a77aa54d53305205901f639c49534b..6f1cf058e38cd1560f4ae1e3b1c5bc1ad1c715b0 100644 --- a/apps/web/src/browser/previewWebviewConfigState.ts +++ b/apps/web/src/browser/previewWebviewConfigState.ts @@ -1,8 +1,12 @@ import { useAtomValue } from "@effect/atom-react"; -import type { DesktopPreviewWebviewConfig, EnvironmentId } from "@t3tools/contracts"; -import * as Data from "effect/Data"; +import type { + DesktopPreviewBridge, + DesktopPreviewWebviewConfig, + EnvironmentId, +} from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { previewBridge } from "~/components/preview/previewBridge"; @@ -10,27 +14,51 @@ import { previewBridge } from "~/components/preview/previewBridge"; const PREVIEW_CONFIG_STALE_TIME_MS = 5 * 60_000; const PREVIEW_CONFIG_IDLE_TTL_MS = 10 * 60_000; -class PreviewWebviewConfigError extends Data.TaggedError("PreviewWebviewConfigError")<{ - readonly message: string; - readonly cause?: unknown; -}> {} +export class PreviewWebviewBridgeUnavailableError extends Schema.TaggedErrorClass()( + "PreviewWebviewBridgeUnavailableError", + { environmentId: Schema.String }, +) { + override get message(): string { + return `Desktop preview configuration is unavailable for environment "${this.environmentId}".`; + } +} + +export class PreviewWebviewConfigLoadError extends Schema.TaggedErrorClass()( + "PreviewWebviewConfigLoadError", + { + environmentId: Schema.String, + cause: Schema.Defect(), + }, +) { + override get message(): string { + return `Failed to load desktop preview configuration for environment "${this.environmentId}".`; + } +} + +export const PreviewWebviewConfigError = Schema.Union([ + PreviewWebviewBridgeUnavailableError, + PreviewWebviewConfigLoadError, +]); +export type PreviewWebviewConfigError = typeof PreviewWebviewConfigError.Type; + +type PreviewConfigBridge = Pick; + +export const loadPreviewWebviewConfig = ( + environmentId: EnvironmentId, + bridge: PreviewConfigBridge | null = previewBridge, +): Effect.Effect => { + if (bridge === null) { + return Effect.fail(new PreviewWebviewBridgeUnavailableError({ environmentId })); + } + + return Effect.tryPromise({ + try: () => bridge.getPreviewConfig(environmentId), + catch: (cause) => new PreviewWebviewConfigLoadError({ environmentId, cause }), + }); +}; const previewWebviewConfigAtom = Atom.family((environmentId: EnvironmentId) => - Atom.make( - Effect.tryPromise({ - try: () => { - if (!previewBridge) { - throw new Error("Desktop preview bridge is unavailable."); - } - return previewBridge.getPreviewConfig(environmentId); - }, - catch: (cause) => - new PreviewWebviewConfigError({ - message: "Could not load desktop preview configuration.", - cause, - }), - }), - ).pipe( + Atom.make(loadPreviewWebviewConfig(environmentId)).pipe( Atom.swr({ staleTime: PREVIEW_CONFIG_STALE_TIME_MS, revalidateOnMount: true,