authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:25:14-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:25:14-07:00
log2910d9ff0106d06f8fc1c55a44787385118f595a
treea76cafdb598aeb182ee2daccab21443b72a845b3
parent2b8e012924063e185786c8ff120b6fd2886b3aa7
signature Signed by PGP key B5690EEEBB952194

[codex] Structure preview config failures (#3271)

Co-authored-by: codex <codex@users.noreply.github.com>

2 files changed, 107 insertions(+), 21 deletions(-)

apps/web/src/browser/previewWebviewConfigState.test.ts created+58
......@@ -0,0 +1,58 @@
1import { describe, expect, it } from "@effect/vitest";
2import { EnvironmentId } from "@t3tools/contracts";
3import * as Effect from "effect/Effect";
4
5import {
6 loadPreviewWebviewConfig,
7 PreviewWebviewBridgeUnavailableError,
8 PreviewWebviewConfigLoadError,
9} from "./previewWebviewConfigState";
10
11const environmentId = EnvironmentId.make("environment-1");
12
13describe("loadPreviewWebviewConfig", () => {
14 it.effect("reports a structurally distinct missing-bridge failure", () =>
15 Effect.gen(function* () {
16 const error = yield* loadPreviewWebviewConfig(environmentId, null).pipe(Effect.flip);
17
18 expect(error).toBeInstanceOf(PreviewWebviewBridgeUnavailableError);
19 expect(error.environmentId).toBe(environmentId);
20 expect(error.message).toContain(environmentId);
21 expect("cause" in error).toBe(false);
22 }),
23 );
24
25 it.effect("preserves the bridge rejection as the load failure cause", () =>
26 Effect.gen(function* () {
27 const cause = new Error("ipc unavailable");
28 const error = yield* loadPreviewWebviewConfig(environmentId, {
29 getPreviewConfig: () => Promise.reject(cause),
30 }).pipe(Effect.flip);
31
32 expect(error).toBeInstanceOf(PreviewWebviewConfigLoadError);
33 expect(error.environmentId).toBe(environmentId);
34 expect(error.cause).toBe(cause);
35 expect(error.message).not.toContain(cause.message);
36 }),
37 );
38
39 it.effect("forwards the environment id to the bridge", () =>
40 Effect.gen(function* () {
41 let requestedEnvironmentId: EnvironmentId | null = null;
42 const config = {
43 partition: "persist:test-preview",
44 webPreferences: "sandbox=yes",
45 preloadUrl: null,
46 };
47 const result = yield* loadPreviewWebviewConfig(environmentId, {
48 getPreviewConfig: (input) => {
49 requestedEnvironmentId = input;
50 return Promise.resolve(config);
51 },
52 });
53
54 expect(requestedEnvironmentId).toBe(environmentId);
55 expect(result).toEqual(config);
56 }),
57 );
58});
apps/web/src/browser/previewWebviewConfigState.ts+49-21
......@@ -1,8 +1,12 @@
11import { useAtomValue } from "@effect/atom-react";
2import type { DesktopPreviewWebviewConfig, EnvironmentId } from "@t3tools/contracts";
3import * as Data from "effect/Data";
2import type {
3 DesktopPreviewBridge,
4 DesktopPreviewWebviewConfig,
5 EnvironmentId,
6} from "@t3tools/contracts";
47import * as Effect from "effect/Effect";
58import * as Option from "effect/Option";
9import * as Schema from "effect/Schema";
610import { AsyncResult, Atom } from "effect/unstable/reactivity";
711
812import { previewBridge } from "~/components/preview/previewBridge";
......@@ -10,27 +14,51 @@ import { previewBridge } from "~/components/preview/previewBridge";
1014const PREVIEW_CONFIG_STALE_TIME_MS = 5 * 60_000;
1115const PREVIEW_CONFIG_IDLE_TTL_MS = 10 * 60_000;
1216
13class PreviewWebviewConfigError extends Data.TaggedError("PreviewWebviewConfigError")<{
14 readonly message: string;
15 readonly cause?: unknown;
16}> {}
17export class PreviewWebviewBridgeUnavailableError extends Schema.TaggedErrorClass<PreviewWebviewBridgeUnavailableError>()(
18 "PreviewWebviewBridgeUnavailableError",
19 { environmentId: Schema.String },
20) {
21 override get message(): string {
22 return `Desktop preview configuration is unavailable for environment "${this.environmentId}".`;
23 }
24}
25
26export class PreviewWebviewConfigLoadError extends Schema.TaggedErrorClass<PreviewWebviewConfigLoadError>()(
27 "PreviewWebviewConfigLoadError",
28 {
29 environmentId: Schema.String,
30 cause: Schema.Defect(),
31 },
32) {
33 override get message(): string {
34 return `Failed to load desktop preview configuration for environment "${this.environmentId}".`;
35 }
36}
37
38export const PreviewWebviewConfigError = Schema.Union([
39 PreviewWebviewBridgeUnavailableError,
40 PreviewWebviewConfigLoadError,
41]);
42export type PreviewWebviewConfigError = typeof PreviewWebviewConfigError.Type;
43
44type PreviewConfigBridge = Pick<DesktopPreviewBridge, "getPreviewConfig">;
45
46export const loadPreviewWebviewConfig = (
47 environmentId: EnvironmentId,
48 bridge: PreviewConfigBridge | null = previewBridge,
49): Effect.Effect<DesktopPreviewWebviewConfig, PreviewWebviewConfigError> => {
50 if (bridge === null) {
51 return Effect.fail(new PreviewWebviewBridgeUnavailableError({ environmentId }));
52 }
53
54 return Effect.tryPromise({
55 try: () => bridge.getPreviewConfig(environmentId),
56 catch: (cause) => new PreviewWebviewConfigLoadError({ environmentId, cause }),
57 });
58};
1759
1860const previewWebviewConfigAtom = Atom.family((environmentId: EnvironmentId) =>
19 Atom.make(
20 Effect.tryPromise({
21 try: () => {
22 if (!previewBridge) {
23 throw new Error("Desktop preview bridge is unavailable.");
24 }
25 return previewBridge.getPreviewConfig(environmentId);
26 },
27 catch: (cause) =>
28 new PreviewWebviewConfigError({
29 message: "Could not load desktop preview configuration.",
30 cause,
31 }),
32 }),
33 ).pipe(
61 Atom.make(loadPreviewWebviewConfig(environmentId)).pipe(
3462 Atom.swr({
3563 staleTime: PREVIEW_CONFIG_STALE_TIME_MS,
3664 revalidateOnMount: true,