Skip to content

Commit 298cb24

Browse files
refactor: errors are Schema.TaggedError, not Data.TaggedError or native Error (#31216)
Co-authored-by: Claude Opus 5.5 <[email protected]>
1 parent 20e92ff commit 298cb24

87 files changed

Lines changed: 903 additions & 673 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/src/ipc/methods/sshEnvironment.ts‎

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
} from "@t3tools/contracts";
2727
import { SshHttpBridgeError } from "@t3tools/ssh/errors";
2828
import { resolveLoopbackSshHttpBaseUrl } from "@t3tools/ssh/tunnel";
29-
import * as Data from "effect/Data";
3029
import * as Effect from "effect/Effect";
3130
import * as Schema from "effect/Schema";
3231

@@ -35,10 +34,12 @@ import * as DesktopIpc from "../DesktopIpc.ts";
3534
import * as DesktopSshEnvironment from "../../ssh/DesktopSshEnvironment.ts";
3635
import * as DesktopSshPasswordPrompts from "../../ssh/DesktopSshPasswordPrompts.ts";
3736

38-
type DesktopSshEnvironmentRequestOperation =
39-
| "fetch-environment-descriptor"
40-
| "bootstrap-bearer-session"
41-
| "fetch-session-state";
37+
const DesktopSshEnvironmentRequestOperation = Schema.Literals([
38+
"fetch-environment-descriptor",
39+
"bootstrap-bearer-session",
40+
"fetch-session-state",
41+
]);
42+
type DesktopSshEnvironmentRequestOperation = typeof DesktopSshEnvironmentRequestOperation.Type;
4243

4344
type DesktopSshEnvironmentRequestCause = RemoteEnvironmentAuthError | SshHttpBridgeError;
4445

@@ -47,12 +48,13 @@ const isEnvironmentInternalError = Schema.is(EnvironmentInternalError);
4748
const isEnvironmentOperationForbiddenError = Schema.is(EnvironmentOperationForbiddenError);
4849
const isEnvironmentRequestInvalidError = Schema.is(EnvironmentRequestInvalidError);
4950
const isEnvironmentScopeRequiredError = Schema.is(EnvironmentScopeRequiredError);
51+
const isRemoteEnvironmentAuthUndeclaredStatusError = Schema.is(
52+
RemoteEnvironmentAuthUndeclaredStatusError,
53+
);
54+
const isSshHttpBridgeError = Schema.is(SshHttpBridgeError);
5055

5156
function readSshHttpStatus(cause: DesktopSshEnvironmentRequestCause): number | null {
52-
if (
53-
cause instanceof RemoteEnvironmentAuthUndeclaredStatusError ||
54-
cause instanceof SshHttpBridgeError
55-
) {
57+
if (isRemoteEnvironmentAuthUndeclaredStatusError(cause) || isSshHttpBridgeError(cause)) {
5658
return cause.status ?? null;
5759
}
5860
if (isEnvironmentRequestInvalidError(cause)) {
@@ -73,13 +75,14 @@ function readSshHttpStatus(cause: DesktopSshEnvironmentRequestCause): number | n
7375
return null;
7476
}
7577

76-
export class DesktopSshEnvironmentRequestError extends Data.TaggedError(
78+
export class DesktopSshEnvironmentRequestError extends Schema.TaggedError<DesktopSshEnvironmentRequestError>()(
7779
"DesktopSshEnvironmentRequestError",
78-
)<{
79-
readonly operation: DesktopSshEnvironmentRequestOperation;
80-
readonly cause: DesktopSshEnvironmentRequestCause;
81-
readonly sshHttpStatus: number | null;
82-
}> {
80+
{
81+
operation: DesktopSshEnvironmentRequestOperation,
82+
cause: Schema.Defect(),
83+
sshHttpStatus: Schema.NullOr(Schema.Number),
84+
},
85+
) {
8386
// The web client matches "[ssh_http:500]" to explain version-skewed SSH pairing.
8487
override get message() {
8588
const prefix = this.sshHttpStatus === null ? "" : `[ssh_http:${this.sshHttpStatus}] `;

‎apps/desktop/src/snapShot/DesktopSnapShot.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1872,7 +1872,9 @@ it.effect("capture fails closed while snapshots are disabled", () => {
18721872

18731873
it.effect("points capture setup failures at SnapShot settings", () => {
18741874
vi.stubEnv("XDG_SESSION_TYPE", "wayland");
1875-
linuxCaptureMock.mockRejectedValueOnce(new CaptureSetupRequiredError("Install the helper."));
1875+
linuxCaptureMock.mockRejectedValueOnce(
1876+
new CaptureSetupRequiredError({ state: "Install the helper." }),
1877+
);
18761878
return Effect.scoped(
18771879
Effect.gen(function* () {
18781880
const service = yield* DesktopSnapShot.make;

‎apps/desktop/src/snapShot/DesktopSnapShot.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ import {
9494
import * as HostProcess from "@t3tools/shared/HostProcess";
9595
import { readImageDimensions } from "@t3tools/shared/imageDimensions";
9696

97+
const isCaptureSetupRequiredError = Schema.is(CaptureSetupRequiredError);
98+
9799
const MAX_CAPTURE_WIDTH = 2_560;
98100
const MAX_CAPTURE_HEIGHT = 1_600;
99101
const SHORTCUT_COOLDOWN_NS = 200_000_000n;
@@ -180,7 +182,7 @@ const shortcutFailure = (cause: unknown) => new SnapShotShortcutError({ cause })
180182

181183
function captureFailure(cause: unknown, captureId?: string): DesktopSnapShotError {
182184
if (isDesktopSnapShotError(cause)) return cause;
183-
const operation = cause instanceof CaptureSetupRequiredError ? "setup-required" : "capture";
185+
const operation = isCaptureSetupRequiredError(cause) ? "setup-required" : "capture";
184186
return new DesktopSnapShotError({ operation, captureId, cause });
185187
}
186188

‎apps/desktop/src/snapShot/HyprlandSnapShot.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,11 +171,11 @@ export async function captureHyprlandWindow(
171171
let state = await setup.state();
172172
if (state.status === "update-required") {
173173
await setup.perform("install-hyprland-helper").catch((cause: unknown) => {
174-
throw new CaptureSetupRequiredError(state.message, { cause });
174+
throw new CaptureSetupRequiredError({ state: state.message, cause });
175175
});
176176
state = await setup.state();
177177
}
178-
if (state.status !== "ready") throw new CaptureSetupRequiredError(state.message);
178+
if (state.status !== "ready") throw new CaptureSetupRequiredError({ state: state.message });
179179
const executable = hyprlandCaptureExecutable(paths);
180180
const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-hyprland-capture-"));
181181
const cleanup = () => NodeFSP.rm(directory, { recursive: true, force: true });

‎apps/desktop/src/snapShot/KdeSnapShot.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export async function captureKdeWindow(
215215
options?: { readonly flash: boolean; readonly animate: boolean },
216216
): Promise<LinuxWindowSnapshot> {
217217
const state = await new KdeCaptureSetup(paths).state();
218-
if (state.status !== "ready") throw new CaptureSetupRequiredError(state.message);
218+
if (state.status !== "ready") throw new CaptureSetupRequiredError({ state: state.message });
219219
const { executable } = kdeCapturePaths(paths);
220220
const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-kde-capture-"));
221221
let retained = false;

‎apps/desktop/src/snapShot/linuxCaptureSession.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
22
import * as NodeFSP from "node:fs/promises";
33
import * as NodeURL from "node:url";
44
import type { SnapShotKeyChord } from "@t3tools/contracts";
5+
import * as Schema from "effect/Schema";
56
import { nativeImage } from "electron";
67

78
// Keep dbus-next out of desktop detection and PNG reads in the main process.
89

910
export const HYPRLAND_CAPTURE_ACTION = "capture-window";
1011

11-
export class CaptureSetupRequiredError extends Error {
12-
constructor(state: string, options?: ErrorOptions) {
13-
super(`${state} Open Settings → SnapShots to continue setup.`, options);
12+
export class CaptureSetupRequiredError extends Schema.TaggedError<CaptureSetupRequiredError>()(
13+
"CaptureSetupRequiredError",
14+
{ state: Schema.String, cause: Schema.optional(Schema.Defect()) },
15+
) {
16+
override get message(): string {
17+
return `${this.state} Open Settings → SnapShots to continue setup.`;
1418
}
1519
}
1620
export const NIRI_CAPTURE_PATH = "/com/t3tools/SnapShot";

‎apps/desktop/src/ssh/DesktopSshEnvironment.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import * as Effect from "effect/Effect";
2323
import * as FileSystem from "effect/FileSystem";
2424
import * as Layer from "effect/Layer";
2525
import * as Path from "effect/Path";
26+
import * as Schema from "effect/Schema";
2627
import * as HttpClient from "effect/http/HttpClient";
2728
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
2829

@@ -74,11 +75,13 @@ function discoverDesktopSshHostsEffect(input?: { readonly homeDir?: string }) {
7475
return discoverSshHosts(input ?? {});
7576
}
7677

78+
const isSshPasswordPromptError = Schema.is(SshPasswordPromptError);
79+
7780
export function isDesktopSshPasswordPromptCancellation(
7881
error: unknown,
7982
): error is SshPasswordPromptError {
8083
return (
81-
error instanceof SshPasswordPromptError &&
84+
isSshPasswordPromptError(error) &&
8285
DesktopSshPasswordPrompts.isDesktopSshPasswordPromptCancellation(error.cause)
8386
);
8487
}

‎apps/mobile/src/components/FilePreviewModal.tsx‎

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as Schema from "effect/Schema";
12
import * as Dialog from "./ui/Dialog";
23
import { useContext, useEffect, useEffectEvent, useState } from "react";
34
import { Platform } from "react-native";
@@ -21,15 +22,17 @@ export type {
2122
ResolvedFilePreviewSource,
2223
} from "./FilePreviewModal.types";
2324

24-
class PreviewResolutionError extends Error {
25-
constructor(
26-
override readonly cause: unknown,
27-
readonly fallbackMessage: string,
28-
) {
29-
super(fallbackMessage, { cause });
25+
class PreviewResolutionError extends Schema.TaggedError<PreviewResolutionError>()(
26+
"PreviewResolutionError",
27+
{ cause: Schema.Defect(), fallbackMessage: Schema.String },
28+
) {
29+
override get message(): string {
30+
return this.fallbackMessage;
3031
}
3132
}
3233

34+
const isPreviewResolutionError = Schema.is(PreviewResolutionError);
35+
3336
interface ResolvedPreviewUri {
3437
readonly uri: string;
3538
readonly dispose?: () => void;
@@ -42,7 +45,10 @@ async function resolvePreviewUri(
4245
if ("uri" in item) return { uri: item.uri };
4346
if ("attachment" in item) {
4447
return loadLocalAttachmentPreview(item.attachment, signal).catch((error: unknown) => {
45-
throw new PreviewResolutionError(error, "Attach the file again and retry.");
48+
throw new PreviewResolutionError({
49+
cause: error,
50+
fallbackMessage: "Attach the file again and retry.",
51+
});
4652
});
4753
}
4854
// Refresh expired URLs before handing them to Quick Look or ACTION_VIEW, which retain them.
@@ -52,10 +58,11 @@ async function resolvePreviewUri(
5258
return value;
5359
})
5460
.catch((error: unknown) => {
55-
throw new PreviewResolutionError(
56-
error,
57-
"Reconnect to this environment and try again. The file may have been moved or deleted.",
58-
);
61+
throw new PreviewResolutionError({
62+
cause: error,
63+
fallbackMessage:
64+
"Reconnect to this environment and try again. The file may have been moved or deleted.",
65+
});
5966
});
6067
return { uri: url + (item.srcFragment ?? "") };
6168
}
@@ -99,9 +106,9 @@ function ResolvedFilePreview(props: {
99106
if (progressiveGallery && index !== selectedIndex) return settle(index, null);
100107
controller.abort();
101108
onResolutionError(
102-
error instanceof PreviewResolutionError
109+
isPreviewResolutionError(error)
103110
? error
104-
: new PreviewResolutionError(error, "Try again."),
111+
: new PreviewResolutionError({ cause: error, fallbackMessage: "Try again." }),
105112
);
106113
},
107114
);

‎apps/mobile/src/features/cloud/dpop.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import * as Digest from "effect/crypto/Digest";
22
import * as SecureRandom from "effect/crypto/SecureRandom";
3-
import * as Data from "effect/Data";
43
import * as Effect from "effect/Effect";
54
import * as Base64Url from "effect/encoding/Base64Url";
65
import * as Hex from "effect/encoding/Hex";
@@ -13,10 +12,10 @@ import { DpopProof } from "@t3tools/client-runtime/relay";
1312
import { DpopPublicJwk } from "@t3tools/shared/dpopCommon";
1413
import * as Layer from "effect/Layer";
1514

16-
export class CloudDpopError extends Data.TaggedError("CloudDpopError")<{
17-
readonly message: string;
18-
readonly cause?: unknown;
19-
}> {}
15+
export class CloudDpopError extends Schema.TaggedError<CloudDpopError>()("CloudDpopError", {
16+
message: Schema.String,
17+
cause: Schema.optional(Schema.Defect()),
18+
}) {}
2019

2120
function cloudDpopError(message: string) {
2221
return (cause: unknown) => new CloudDpopError({ message, cause });

‎apps/mobile/src/features/cloud/linkEnvironment.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as Data from "effect/Data";
21
import * as Effect from "effect/Effect";
32
import * as Schema from "effect/Schema";
43
import { HttpClient } from "effect/http";
@@ -26,11 +25,14 @@ function readRelayUrl(): string | null {
2625
return resolveCloudPublicConfig().relay.url;
2726
}
2827

29-
export class CloudEnvironmentLinkError extends Data.TaggedError("CloudEnvironmentLinkError")<{
30-
readonly message: string;
31-
readonly cause?: unknown;
32-
readonly traceId?: string;
33-
}> {}
28+
export class CloudEnvironmentLinkError extends Schema.TaggedError<CloudEnvironmentLinkError>()(
29+
"CloudEnvironmentLinkError",
30+
{
31+
message: Schema.String,
32+
cause: Schema.optional(Schema.Defect()),
33+
traceId: Schema.optional(Schema.String),
34+
},
35+
) {}
3436

3537
const isEnvironmentCloudApiError = Schema.is(
3638
Schema.Union([

0 commit comments

Comments
 (0)