authorgravatar for ethorelli@gmail.comErik Thorelli <ethorelli@gmail.com> 2026-10-05 21:49:13-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-05 21:49:13-07:00
logff397a29523d3c63e5844b11608c0189640c7561
treebd5179ac2bcd68f267b03c130c2adc7a8b6336d8
parentfad673225650193fad2a85487ecffc903f8ff30a
signature Signed by PGP key B5690EEEBB952194

chore(lint): catch known tags with Effect.catchTags (#16361)


21 files changed, 220 insertions(+), 85 deletions(-)

apps/desktop/src/backend/DesktopBackendManager.ts+1-1
...@@ -423,7 +423,7 @@ function drainBackendOutput(...@@ -423,7 +423,7 @@ function drainBackendOutput(
423 cause,423 cause,
424 }),424 }),
425 ),425 ),
426 Effect.catchTag("BackendProcessOutputHandlingError", onOutputFailure),426 Effect.catchTags({ BackendProcessOutputHandlingError: onOutputFailure }),
427 ),427 ),
428 ),428 ),
429 Effect.catchTags({429 Effect.catchTags({
apps/server/src/auth/http.ts+5-4
...@@ -223,7 +223,7 @@ export const layerAuthenticatedAuth = Layer.effect(...@@ -223,7 +223,7 @@ export const layerAuthenticatedAuth = Layer.effect(
223 }),223 }),
224 session.subject === "cloud-connect" ? traceAuthenticatedRelayRequest : identity,224 session.subject === "cloud-connect" ? traceAuthenticatedRelayRequest : identity,
225 );225 );
226 }).pipe(Effect.catchTag("EnvironmentAuthInvalidError", appendDpopChallengeOnUnauthorized));226 }).pipe(Effect.catchTags({ EnvironmentAuthInvalidError: appendDpopChallengeOnUnauthorized }));
227 }),227 }),
228);228);
229229
...@@ -474,9 +474,10 @@ export const layer = HttpApiBuilder.group(...@@ -474,9 +474,10 @@ export const layer = HttpApiBuilder.group(
474 );474 );
475 return { revoked };475 return { revoked };
476 },476 },
477 Effect.catchTag("ServerAuthForbiddenOperationError", () =>477 Effect.catchTags({
478 failEnvironmentOperationForbidden("current_session_revoke_not_allowed"),478 ServerAuthForbiddenOperationError: () =>
479 ),479 failEnvironmentOperationForbidden("current_session_revoke_not_allowed"),
480 }),
480 Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) =>481 Effect.catchIf(EnvironmentAuth.isServerAuthInternalError, (error) =>
481 failEnvironmentInternal("client_session_revoke_failed", error),482 failEnvironmentInternal("client_session_revoke_failed", error),
482 ),483 ),
apps/server/src/cli/connect.ts+6-5
...@@ -105,11 +105,12 @@ const authorizeCli = Effect.fn("cloud.cli.authorize")(function* (options: {...@@ -105,11 +105,12 @@ const authorizeCli = Effect.fn("cloud.cli.authorize")(function* (options: {
105 // A stored credential whose refresh fails (revoked, expired grant) must105 // A stored credential whose refresh fails (revoked, expired grant) must
106 // fall through to a fresh device authorization, not dead-end the command.106 // fall through to a fresh device authorization, not dead-end the command.
107 const existing = yield* tokens.getExisting.pipe(107 const existing = yield* tokens.getExisting.pipe(
108 Effect.catchTag("CloudCliCredentialRefreshError", () =>108 Effect.catchTags({
109 Console.log(109 CloudCliCredentialRefreshError: () =>
110 "The stored T3 Connect credential could not be refreshed; signing in again.",110 Console.log(
111 ).pipe(Effect.as(Option.none())),111 "The stored T3 Connect credential could not be refreshed; signing in again.",
112 ),112 ).pipe(Effect.as(Option.none())),
113 }),
113 );114 );
114 if (Option.isSome(existing)) {115 if (Option.isSome(existing)) {
115 return existing.value.identity ?? null;116 return existing.value.identity ?? null;
apps/server/src/cli/uninstall.ts+1-1
...@@ -162,7 +162,7 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: {...@@ -162,7 +162,7 @@ const runUninstall = Effect.fn("cli.uninstall.run")(function* (input: {
162 }162 }
163 const confirmed = yield* Prompt.run(163 const confirmed = yield* Prompt.run(
164 Prompt.Confirm({ message: "Remove t3 from this machine?", initial: false }),164 Prompt.Confirm({ message: "Remove t3 from this machine?", initial: false }),
165 ).pipe(Effect.catchTag("QuitError", () => Effect.succeed(false)));165 ).pipe(Effect.catchTags({ QuitError: () => Effect.succeed(false) }));
166 if (!confirmed) {166 if (!confirmed) {
167 yield* Console.log("Left as is.");167 yield* Console.log("Left as is.");
168 return;168 return;
apps/server/src/cli/update.ts+2-2
...@@ -384,7 +384,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: {...@@ -384,7 +384,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: {
384 }384 }
385 const confirmed = yield* Prompt.run(385 const confirmed = yield* Prompt.run(
386 Prompt.Confirm({ message: "Install the preview build anyway?", initial: false }),386 Prompt.Confirm({ message: "Install the preview build anyway?", initial: false }),
387 ).pipe(Effect.catchTag("QuitError", () => Effect.succeed(false)));387 ).pipe(Effect.catchTags({ QuitError: () => Effect.succeed(false) }));
388 if (!confirmed) {388 if (!confirmed) {
389 yield* Console.log("Left as is.");389 yield* Console.log("Left as is.");
390 return;390 return;
...@@ -473,7 +473,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: {...@@ -473,7 +473,7 @@ const runUpdate = Effect.fn("cli.update.run")(function* (input: {
473 message: "Restart the background service once the download is verified?",473 message: "Restart the background service once the download is verified?",
474 initial: true,474 initial: true,
475 }),475 }),
476 ).pipe(Effect.catchTag("QuitError", () => Effect.succeed(false)));476 ).pipe(Effect.catchTags({ QuitError: () => Effect.succeed(false) }));
477 } else {477 } else {
478 yield* Console.log(478 yield* Console.log(
479 " Not a terminal, so the service keeps running its current version. Rerun with --yes to restart it now, or run `t3 service restart` later.",479 " Not a terminal, so the service keeps running its current version. Rerun with --yes to restart it now, or run `t3 service restart` later.",
apps/server/src/cloud/CliTokenManager.ts+6-6
...@@ -408,9 +408,9 @@ export const deviceAuthorizationLogin = Effect.fn("cloud.cli_token.device_author...@@ -408,9 +408,9 @@ export const deviceAuthorizationLogin = Effect.fn("cloud.cli_token.device_author
408 });408 });
409 return yield* pollDeviceToken(metadata, authorization.device_code, interval).pipe(409 return yield* pollDeviceToken(metadata, authorization.device_code, interval).pipe(
410 Effect.timeout(expiresIn),410 Effect.timeout(expiresIn),
411 Effect.catchTag("TimeoutError", (cause) =>411 Effect.catchTags({
412 Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })),412 TimeoutError: (cause) => Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })),
413 ),413 }),
414 );414 );
415 },415 },
416);416);
...@@ -505,9 +505,9 @@ export const make = Effect.gen(function* () {...@@ -505,9 +505,9 @@ export const make = Effect.gen(function* () {
505 authorizationUrl,505 authorizationUrl,
506 callback: Deferred.await(callback).pipe(506 callback: Deferred.await(callback).pipe(
507 Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),507 Effect.timeout(CLOUD_CLI_OAUTH_CALLBACK_TIMEOUT),
508 Effect.catchTag("TimeoutError", (cause) =>508 Effect.catchTags({
509 Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })),509 TimeoutError: (cause) => Effect.fail(new CloudCliAuthorizationTimeoutError({ cause })),
510 ),510 }),
511 ),511 ),
512 terminal,512 terminal,
513 launchBrowser: externalLauncher.launchBrowser,513 launchBrowser: externalLauncher.launchBrowser,
apps/server/src/cloud/CloudLink.ts+3-3
...@@ -681,7 +681,7 @@ const make = Effect.gen(function* () {...@@ -681,7 +681,7 @@ const make = Effect.gen(function* () {
681 return proof satisfies RelayEnvironmentLinkProof;681 return proof satisfies RelayEnvironmentLinkProof;
682 },682 },
683 Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("generate-link-proof")),683 Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("generate-link-proof")),
684 Effect.catchTag("PlatformError", internalError("generate-link-proof")),684 Effect.catchTags({ PlatformError: internalError("generate-link-proof") }),
685 );685 );
686686
687 const activateManagedTunnel = Effect.fn("environment.cloud.activateManagedTunnel")(687 const activateManagedTunnel = Effect.fn("environment.cloud.activateManagedTunnel")(
...@@ -1546,7 +1546,7 @@ const make = Effect.gen(function* () {...@@ -1546,7 +1546,7 @@ const make = Effect.gen(function* () {
1546 } satisfies RelayEnvironmentHealthResponse;1546 } satisfies RelayEnvironmentHealthResponse;
1547 },1547 },
1548 Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("answer-health")),1548 Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("answer-health")),
1549 Effect.catchTag("PlatformError", internalError("answer-health")),1549 Effect.catchTags({ PlatformError: internalError("answer-health") }),
1550 );1550 );
15511551
1552 const mintCredential = Effect.fn("environment.cloud.mintCredential")(1552 const mintCredential = Effect.fn("environment.cloud.mintCredential")(
...@@ -1653,7 +1653,7 @@ const make = Effect.gen(function* () {...@@ -1653,7 +1653,7 @@ const make = Effect.gen(function* () {
1653 } satisfies RelayEnvironmentMintResponse;1653 } satisfies RelayEnvironmentMintResponse;
1654 },1654 },
1655 Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("issue-credential")),1655 Effect.catchIf(ServerSecretStore.isSecretStoreError, internalError("issue-credential")),
1656 Effect.catchTag("PlatformError", internalError("issue-credential")),1656 Effect.catchTags({ PlatformError: internalError("issue-credential") }),
1657 );1657 );
16581658
1659 return CloudLink.of({1659 return CloudLink.of({
apps/server/src/htmlRender/HtmlRender.ts+20-18
...@@ -351,26 +351,28 @@ const make = Effect.gen(function* () {...@@ -351,26 +351,28 @@ const make = Effect.gen(function* () {
351 browsers.withPermits(1)(351 browsers.withPermits(1)(
352 // Read once a permit is held, so a queued call sees a fallback learned meanwhile.352 // Read once a permit is held, so a queued call sees a fallback learned meanwhile.
353 Effect.suspend(() => run(noSandbox)).pipe(353 Effect.suspend(() => run(noSandbox)).pipe(
354 Effect.catchTag("HtmlRenderSandboxUnavailableError", () =>354 Effect.catchTags({
355 Effect.logInfo(355 HtmlRenderSandboxUnavailableError: () =>
356 "Chrome's sandbox is unavailable on this host; launching it without one.",356 Effect.logInfo(
357 ).pipe(357 "Chrome's sandbox is unavailable on this host; launching it without one.",
358 Effect.andThen(358 ).pipe(
359 Effect.sync(() => {359 Effect.andThen(
360 noSandbox = true;360 Effect.sync(() => {
361 noSandbox = true;
362 }),
363 ),
364 Effect.andThen(run(true)),
365 ),
366 }),
367 Effect.catchTags({
368 HtmlRenderSandboxUnavailableError: (cause) =>
369 Effect.fail(
370 new HeadlessChrome.HtmlRenderBrowserError({
371 reason: "the browser has no sandbox",
372 cause,
361 }),373 }),
362 ),374 ),
363 Effect.andThen(run(true)),375 }),
364 ),
365 ),
366 Effect.catchTag("HtmlRenderSandboxUnavailableError", (cause) =>
367 Effect.fail(
368 new HeadlessChrome.HtmlRenderBrowserError({
369 reason: "the browser has no sandbox",
370 cause,
371 }),
372 ),
373 ),
374 ),376 ),
375 );377 );
376378
apps/server/src/orchestration-v2/Adapters/OpenCodeAdapterV2.ts+12-10
...@@ -3011,16 +3011,18 @@ export function makeOpenCodeAdapterV2(...@@ -3011,16 +3011,18 @@ export function makeOpenCodeAdapterV2(
3011 );3011 );
3012 yield* Effect.raceFirst(Fiber.join(request), Deferred.await(receipt)).pipe(3012 yield* Effect.raceFirst(Fiber.join(request), Deferred.await(receipt)).pipe(
3013 Effect.timeout("10 seconds"),3013 Effect.timeout("10 seconds"),
3014 Effect.catchTag("TimeoutError", (cause) => {3014 Effect.catchTags({
3015 const error = new OpenCodeRuntime.OpenCodeRuntimeError({3015 TimeoutError: (cause) => {
3016 operation: "session.command",3016 const error = new OpenCodeRuntime.OpenCodeRuntimeError({
3017 detail: "OpenCode command admission did not complete within 10 seconds.",3017 operation: "session.command",
3018 cause,3018 detail: "OpenCode command admission did not complete within 10 seconds.",
3019 });3019 cause,
3020 abortController.abort();3020 });
3021 return finalizeTurn(state, turn, "failed", {3021 abortController.abort();
3022 failure: makeProviderFailure({ cause: error, class: "provider_error" }),3022 return finalizeTurn(state, turn, "failed", {
3023 }).pipe(Effect.andThen(Effect.fail(error)));3023 failure: makeProviderFailure({ cause: error, class: "provider_error" }),
3024 }).pipe(Effect.andThen(Effect.fail(error)));
3025 },
3024 }),3026 }),
3025 );3027 );
3026 });3028 });
apps/server/src/provider/acp/AcpRegistrySupport.ts+3-4
...@@ -1916,16 +1916,15 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct...@@ -1916,16 +1916,15 @@ export const makeAcpRegistryCatalog = Effect.fn("AcpRegistryCatalog.make")(funct
1916 ),1916 ),
1917 )1917 )
1918 .pipe(1918 .pipe(
1919 Effect.catchTag(1919 Effect.catchTags({
1920 "ServerSettingsError",1920 ServerSettingsError: (cause) =>
1921 (cause) =>
1922 new AcpRegistryError({1921 new AcpRegistryError({
1923 reason: "install_failed",1922 reason: "install_failed",
1924 detail:1923 detail:
1925 "Could not read provider settings while checking managed ACP binary references.",1924 "Could not read provider settings while checking managed ACP binary references.",
1926 cause,1925 cause,
1927 }),1926 }),
1928 ),1927 }),
1929 );1928 );
19301929
1931 return AcpRegistryCatalog.of({1930 return AcpRegistryCatalog.of({
apps/server/src/pullRequest/PullRequestService.ts+1-1
...@@ -1578,7 +1578,7 @@ export const make = Effect.gen(function* () {...@@ -1578,7 +1578,7 @@ export const make = Effect.gen(function* () {
1578 ? operation.pipe(Effect.provideService(routingCredential, identity), Effect.result)1578 ? operation.pipe(Effect.provideService(routingCredential, identity), Effect.result)
1579 : Effect.fail(rejected()),1579 : Effect.fail(rejected()),
1580 )1580 )
1581 .pipe(Effect.catchTag("PullRequestProviderError", () => Effect.fail(rejected())));1581 .pipe(Effect.catchTags({ PullRequestProviderError: () => Effect.fail(rejected()) }));
1582 return yield* Effect.fromResult(result);1582 return yield* Effect.fromResult(result);
1583 });1583 });
15841584
apps/server/src/workspace/WorkspaceSearchIndex.ts+8-7
...@@ -376,13 +376,14 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (...@@ -376,13 +376,14 @@ export const make = Effect.fn("WorkspaceSearchIndex.make")(function* (
376 cause,376 cause,
377 }),377 }),
378 ).pipe(378 ).pipe(
379 Effect.catchTag("WorkspaceSearchIndexScanTimedOut", (error) =>379 Effect.catchTags({
380 variant === "paths"380 WorkspaceSearchIndexScanTimedOut: (error) =>
381 ? Effect.sync(() => {381 variant === "paths"
382 initialScanTimedOut = true;382 ? Effect.sync(() => {
383 })383 initialScanTimedOut = true;
384 : Effect.fail(error),384 })
385 ),385 : Effect.fail(error),
386 }),
386 );387 );
387388
388 const hasIncompleteInitialScan = () => initialScanTimedOut && finder.isScanning();389 const hasIncompleteInitialScan = () => initialScanTimedOut && finder.isScanning();
apps/server/src/ws.ts+10-9
...@@ -2697,15 +2697,16 @@ const layerWsRpc = (...@@ -2697,15 +2697,16 @@ const layerWsRpc = (
2697 status,2697 status,
2698 }),2698 }),
2699 ),2699 ),
2700 Effect.catchTag("RelayClientInstallError", (error) =>2700 Effect.catchTags({
2701 Queue.fail(2701 RelayClientInstallError: (error) =>
2702 queue,2702 Queue.fail(
2703 new RelayClientInstallFailedError({2703 queue,
2704 reason: error.reason,2704 new RelayClientInstallFailedError({
2705 message: error.message,2705 reason: error.reason,
2706 }),2706 message: error.message,
2707 ),2707 }),
2708 ),2708 ),
2709 }),
2709 Effect.andThen(Queue.end(queue)),2710 Effect.andThen(Queue.end(queue)),
2710 Effect.forkScoped,2711 Effect.forkScoped,
2711 ),2712 ),
oxlint-plugin-t3code/index.ts+2
...@@ -9,6 +9,7 @@ import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-e...@@ -9,6 +9,7 @@ import noMobileUniwindThemeEscapeHatches from "./rules/no-mobile-uniwind-theme-e
9import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts";9import noNativeTitleTooltip from "./rules/no-native-title-tooltip.ts";
10import noTestInLoop from "./rules/no-test-in-loop.ts";10import noTestInLoop from "./rules/no-test-in-loop.ts";
11import noUnscopedHas from "./rules/no-unscoped-has.ts";11import noUnscopedHas from "./rules/no-unscoped-has.ts";
12import preferCatchTags from "./rules/prefer-catch-tags.ts";
12import requireSuppressionReason from "./rules/require-suppression-reason.ts";13import requireSuppressionReason from "./rules/require-suppression-reason.ts";
1314
14export default definePlugin({15export default definePlugin({
...@@ -25,6 +26,7 @@ export default definePlugin({...@@ -25,6 +26,7 @@ export default definePlugin({
25 "no-native-title-tooltip": noNativeTitleTooltip,26 "no-native-title-tooltip": noNativeTitleTooltip,
26 "no-test-in-loop": noTestInLoop,27 "no-test-in-loop": noTestInLoop,
27 "no-unscoped-has": noUnscopedHas,28 "no-unscoped-has": noUnscopedHas,
29 "prefer-catch-tags": preferCatchTags,
28 "require-suppression-reason": requireSuppressionReason,30 "require-suppression-reason": requireSuppressionReason,
29 },31 },
30});32});
oxlint-plugin-t3code/rules/prefer-catch-tags.test.ts created+64
...@@ -0,0 +1,64 @@
1import { assert, describe } from "@effect/vitest";
2
3import { createOxlintRuleHarness } from "../test/utils.ts";
4
5const rule = createOxlintRuleHarness("t3code/prefer-catch-tags");
6
7describe("t3code/prefer-catch-tags", () => {
8 rule.valid(
9 "allows Effect.catchTags",
10 `
11 import * as Effect from "effect/Effect";
12
13 export const program = Effect.fail({ _tag: "A" } as const).pipe(
14 Effect.catchTags({ A: () => Effect.void }),
15 );
16 `,
17 );
18
19 rule.valid(
20 "ignores catchTag on other modules",
21 `
22 import * as Stream from "effect/Stream";
23
24 export const recover = Stream.catchTag("A", () => Stream.empty);
25 `,
26 );
27
28 rule.valid(
29 "ignores a local that shadows the namespace",
30 `
31 import * as Effect from "effect/Effect";
32
33 export const program = Effect.void;
34 export function recover(Effect: { catchTag: (tag: string) => void }) {
35 Effect.catchTag("A");
36 }
37 `,
38 );
39
40 rule.invalid(
41 "reports Effect.catchTag",
42 `
43 import * as Effect from "effect/Effect";
44
45 export const program = Effect.fail({ _tag: "A" } as const).pipe(
46 Effect.catchTag("A", () => Effect.void),
47 );
48 `,
49 (output) => {
50 assert.match(output, /Effect\.catchTags\(\{ Tag: handler \}\)/);
51 },
52 );
53
54 rule.invalid(
55 "reports catchTag through an aliased namespace",
56 `
57 import * as Eff from "effect/Effect";
58
59 export const recover = Eff.catchTag("A", () => Eff.void);
60 `,
61 );
62
63 rule.invalid("reports a named catchTag import", `import { catchTag } from "effect/Effect";`);
64});
oxlint-plugin-t3code/rules/prefer-catch-tags.ts created+60
...@@ -0,0 +1,60 @@
1import { defineRule, type ESTree } from "@oxlint/plugins";
2
3const MESSAGE = "Catch known tags with `Effect.catchTags({ Tag: handler })`, even for one tag.";
4
5/** Reports `Effect.catchTag`, through any namespace import of `effect/Effect` or a named import. */
6export default defineRule({
7 meta: {
8 type: "suggestion",
9 docs: {
10 description: "Require `Effect.catchTags` over `Effect.catchTag`.",
11 },
12 },
13 create(context) {
14 // Whether `identifier` is bound by `import * as X from "effect/Effect"`, not a shadowing local.
15 const isEffectNamespace = (identifier: ESTree.IdentifierReference) => {
16 let scope = context.sourceCode.getScope(identifier);
17 while (true) {
18 const variable = scope.set.get(identifier.name);
19 if (variable !== undefined) {
20 return variable.defs.some(
21 (def) =>
22 def.type === "ImportBinding" &&
23 def.node.type === "ImportNamespaceSpecifier" &&
24 def.parent?.type === "ImportDeclaration" &&
25 def.parent.source.value === "effect/Effect",
26 );
27 }
28 if (scope.upper === null) return false;
29 scope = scope.upper;
30 }
31 };
32
33 return {
34 ImportDeclaration(node) {
35 if (node.source.value !== "effect/Effect") return;
36 for (const specifier of node.specifiers) {
37 if (
38 specifier.type === "ImportSpecifier" &&
39 (specifier.imported.type === "Identifier"
40 ? specifier.imported.name
41 : specifier.imported.value) === "catchTag"
42 ) {
43 context.report({ node: specifier, message: MESSAGE });
44 }
45 }
46 },
47 MemberExpression(node) {
48 if (
49 !node.computed &&
50 node.object.type === "Identifier" &&
51 node.property.type === "Identifier" &&
52 node.property.name === "catchTag" &&
53 isEffectNamespace(node.object)
54 ) {
55 context.report({ node, message: MESSAGE });
56 }
57 },
58 };
59 },
60});
packages/client-runtime/src/connection/registry.ts+3-3
...@@ -482,7 +482,7 @@ export const make = Effect.gen(function* () {...@@ -482,7 +482,7 @@ export const make = Effect.gen(function* () {
482 persistedRoutesByEnvironment.keys(),482 persistedRoutesByEnvironment.keys(),
483 (environmentId) =>483 (environmentId) =>
484 acquireSupervisor(environmentId).pipe(484 acquireSupervisor(environmentId).pipe(
485 Effect.catchTag("EnvironmentNotRegisteredError", () => Effect.void),485 Effect.catchTags({ EnvironmentNotRegisteredError: () => Effect.void }),
486 ),486 ),
487 {487 {
488 concurrency: "unbounded",488 concurrency: "unbounded",
...@@ -999,7 +999,7 @@ export const make = Effect.gen(function* () {...@@ -999,7 +999,7 @@ export const make = Effect.gen(function* () {
999 relayEnvironmentIds,999 relayEnvironmentIds,
1000 (environmentId) =>1000 (environmentId) =>
1001 removeRoute(environmentId, RELAY_ROUTE_ID).pipe(1001 removeRoute(environmentId, RELAY_ROUTE_ID).pipe(
1002 Effect.catchTag("EnvironmentNotRegisteredError", () => Effect.void),1002 Effect.catchTags({ EnvironmentNotRegisteredError: () => Effect.void }),
1003 ),1003 ),
1004 {1004 {
1005 concurrency: "unbounded",1005 concurrency: "unbounded",
...@@ -1012,7 +1012,7 @@ export const make = Effect.gen(function* () {...@@ -1012,7 +1012,7 @@ export const make = Effect.gen(function* () {
1012 const retryNow = (environmentId: EnvironmentId) =>1012 const retryNow = (environmentId: EnvironmentId) =>
1013 acquireSupervisor(environmentId).pipe(1013 acquireSupervisor(environmentId).pipe(
1014 Effect.flatMap((supervisor) => supervisor.retryNow),1014 Effect.flatMap((supervisor) => supervisor.retryNow),
1015 Effect.catchTag("EnvironmentNotRegisteredError", () => Effect.void),1015 Effect.catchTags({ EnvironmentNotRegisteredError: () => Effect.void }),
1016 Effect.withSpan("EnvironmentRegistry.retryNow"),1016 Effect.withSpan("EnvironmentRegistry.retryNow"),
1017 );1017 );
1018 const setEnabled = Effect.fn("EnvironmentRegistry.setEnabled")(function* (1018 const setEnabled = Effect.fn("EnvironmentRegistry.setEnabled")(function* (
packages/shared/src/relayClient.ts+10-9
...@@ -379,15 +379,16 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function...@@ -379,15 +379,16 @@ export const makeCloudflaredRelayClient = Effect.fn("cloudflared.make")(function
379 );379 );
380 yield* report("waiting_for_lock");380 yield* report("waiting_for_lock");
381 yield* acquireInstallLock(lockPath).pipe(381 yield* acquireInstallLock(lockPath).pipe(
382 Effect.catchTag("PlatformError", (cause) =>382 Effect.catchTags({
383 Effect.fail(383 PlatformError: (cause) =>
384 new RelayClientInstallError({384 Effect.fail(
385 reason: "write_failed",385 new RelayClientInstallError({
386 message: "Could not acquire the relay client installation lock.",386 reason: "write_failed",
387 cause,387 message: "Could not acquire the relay client installation lock.",
388 }),388 cause,
389 ),389 }),
390 ),390 ),
391 }),
391 );392 );
392 return yield* Effect.gen(function* () {393 return yield* Effect.gen(function* () {
393 const afterLock = yield* resolve;394 const afterLock = yield* resolve;
packages/shared/src/shell.ts+1-1
...@@ -756,7 +756,7 @@ export const isCommandAvailable = Effect.fn("shell.isCommandAvailable")(function...@@ -756,7 +756,7 @@ export const isCommandAvailable = Effect.fn("shell.isCommandAvailable")(function
756) {756) {
757 return yield* resolveCommandPath(command, options).pipe(757 return yield* resolveCommandPath(command, options).pipe(
758 Effect.as(true),758 Effect.as(true),
759 Effect.catchTag("CommandResolutionError", () => Effect.succeed(false)),759 Effect.catchTags({ CommandResolutionError: () => Effect.succeed(false) }),
760 );760 );
761});761});
762762
scripts/mobile-native-client.ts+1-1
...@@ -316,7 +316,7 @@ const main = Command.make(...@@ -316,7 +316,7 @@ const main = Command.make(
316 installedBinary: installedBinary(platform, device),316 installedBinary: installedBinary(platform, device),
317 readRecord: fs.readFileString(recordPath).pipe(317 readRecord: fs.readFileString(recordPath).pipe(
318 Effect.flatMap(decodeRecord),318 Effect.flatMap(decodeRecord),
319 Effect.catchTag("SchemaError", () => Effect.succeed(null)),319 Effect.catchTags({ SchemaError: () => Effect.succeed(null) }),
320 Effect.catchIf(320 Effect.catchIf(
321 (error) => error.reason._tag === "NotFound",321 (error) => error.reason._tag === "NotFound",
322 () => Effect.succeed(null),322 () => Effect.succeed(null),
vite.config.ts+1
...@@ -170,6 +170,7 @@ export default defineConfig({...@@ -170,6 +170,7 @@ export default defineConfig({
170 "t3code/no-test-in-loop": "error",170 "t3code/no-test-in-loop": "error",
171 "t3code/no-unscoped-has": "error",171 "t3code/no-unscoped-has": "error",
172 "t3code/namespace-node-imports": "error",172 "t3code/namespace-node-imports": "error",
173 "t3code/prefer-catch-tags": "error",
173 "t3code/require-suppression-reason": "error",174 "t3code/require-suppression-reason": "error",
174 },175 },
175 overrides: [176 overrides: [