authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:26:03-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:26:03-07:00
log48f88d522047198a660cacebbde854a617068720
tree739cea09a4c03fea1a3f5deb67accb6ba65851b6
parent2910d9ff0106d06f8fc1c55a44787385118f595a
signature Signed by PGP key B5690EEEBB952194

[codex] Structure desktop Clerk bridge failures (#3308)

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

2 files changed, 114 insertions(+), 15 deletions(-)

apps/desktop/src/app/DesktopClerk.test.ts+67-12
...@@ -1,7 +1,8 @@...@@ -1,7 +1,8 @@
1import { assert, describe, it } from "@effect/vitest";1import { assert, describe, it } from "@effect/vitest";
2import * as Cause from "effect/Cause";
2import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
4import { vi } from "vite-plus/test";5import { beforeEach, vi } from "vite-plus/test";
56
6const { createClerkBridgeMock, storageAdapter, storageMock } = vi.hoisted(() => ({7const { createClerkBridgeMock, storageAdapter, storageMock } = vi.hoisted(() => ({
7 createClerkBridgeMock: vi.fn(),8 createClerkBridgeMock: vi.fn(),
...@@ -24,7 +25,23 @@ vi.mock("@clerk/electron/storage", () => ({...@@ -24,7 +25,23 @@ vi.mock("@clerk/electron/storage", () => ({
24import * as DesktopClerk from "./DesktopClerk.ts";25import * as DesktopClerk from "./DesktopClerk.ts";
25import * as DesktopEnvironment from "./DesktopEnvironment.ts";26import * as DesktopEnvironment from "./DesktopEnvironment.ts";
2627
28const makeDesktopClerkLayer = (isDevelopment = true) => {
29 const environment = DesktopEnvironment.DesktopEnvironment.of({
30 stateDir: "/tmp/t3-state",
31 isDevelopment,
32 } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
33
34 return DesktopClerk.layer.pipe(
35 Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)),
36 );
37};
38
27describe("DesktopClerk", () => {39describe("DesktopClerk", () => {
40 beforeEach(() => {
41 createClerkBridgeMock.mockReset();
42 storageMock.mockReset();
43 });
44
28 it("derives the Clerk Frontend API hostname used by the desktop CSP", () => {45 it("derives the Clerk Frontend API hostname used by the desktop CSP", () => {
29 const publishableKey = `pk_test_${btoa("clerk.t3.codes$")}`;46 const publishableKey = `pk_test_${btoa("clerk.t3.codes$")}`;
3047
...@@ -40,19 +57,9 @@ describe("DesktopClerk", () => {...@@ -40,19 +57,9 @@ describe("DesktopClerk", () => {
40 const cleanup = vi.fn();57 const cleanup = vi.fn();
41 storageMock.mockReturnValue(storageAdapter);58 storageMock.mockReturnValue(storageAdapter);
42 createClerkBridgeMock.mockReturnValue({ cleanup });59 createClerkBridgeMock.mockReturnValue({ cleanup });
43 const environment = DesktopEnvironment.DesktopEnvironment.of({
44 stateDir: "/tmp/t3-state",
45 isDevelopment: true,
46 } as unknown as DesktopEnvironment.DesktopEnvironment["Service"]);
4760
48 return Effect.gen(function* () {61 return Effect.gen(function* () {
49 yield* Effect.scoped(62 yield* Effect.scoped(Layer.build(makeDesktopClerkLayer()));
50 Layer.build(
51 DesktopClerk.layer.pipe(
52 Layer.provide(Layer.succeed(DesktopEnvironment.DesktopEnvironment, environment)),
53 ),
54 ),
55 );
5663
57 assert.deepEqual(createClerkBridgeMock.mock.calls, [64 assert.deepEqual(createClerkBridgeMock.mock.calls, [
58 [65 [
...@@ -69,6 +76,54 @@ describe("DesktopClerk", () => {...@@ -69,6 +76,54 @@ describe("DesktopClerk", () => {
69 });76 });
70 });77 });
7178
79 it.effect("preserves bridge initialization failures", () => {
80 const cause = new Error("bridge initialization failed");
81 storageMock.mockReturnValue(storageAdapter);
82 createClerkBridgeMock.mockImplementationOnce(() => {
83 throw cause;
84 });
85
86 return Effect.gen(function* () {
87 const error = yield* Effect.scoped(Layer.build(makeDesktopClerkLayer())).pipe(Effect.flip);
88
89 assert.instanceOf(error, DesktopClerk.DesktopClerkBridgeInitializationError);
90 assert.equal(error.stateDir, "/tmp/t3-state");
91 assert.equal(error.isDevelopment, true);
92 assert.strictEqual(error.cause, cause);
93 assert.equal(
94 error.message,
95 'Failed to initialize the desktop Clerk bridge for state directory "/tmp/t3-state" (development: true).',
96 );
97 });
98 });
99
100 it.effect("preserves bridge cleanup failures", () => {
101 const cause = new Error("bridge cleanup failed");
102 storageMock.mockReturnValue(storageAdapter);
103 createClerkBridgeMock.mockReturnValue({
104 cleanup: () => {
105 throw cause;
106 },
107 });
108
109 return Effect.gen(function* () {
110 const exit = yield* Effect.exit(Effect.scoped(Layer.build(makeDesktopClerkLayer(false))));
111
112 assert.equal(exit._tag, "Failure");
113 if (exit._tag === "Failure") {
114 const error = Cause.squash(exit.cause);
115 assert.instanceOf(error, DesktopClerk.DesktopClerkBridgeCleanupError);
116 assert.equal(error.stateDir, "/tmp/t3-state");
117 assert.equal(error.isDevelopment, false);
118 assert.strictEqual(error.cause, cause);
119 assert.equal(
120 error.message,
121 'Failed to clean up the desktop Clerk bridge for state directory "/tmp/t3-state" (development: false).',
122 );
123 }
124 });
125 });
126
72 it.each([127 it.each([
73 { isDevelopment: true, scheme: "t3code-dev" },128 { isDevelopment: true, scheme: "t3code-dev" },
74 { isDevelopment: false, scheme: "t3code" },129 { isDevelopment: false, scheme: "t3code" },
apps/desktop/src/app/DesktopClerk.ts+47-3
...@@ -4,6 +4,7 @@ import * as Context from "effect/Context";...@@ -4,6 +4,7 @@ import * as Context from "effect/Context";
4import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
6import * as Option from "effect/Option";6import * as Option from "effect/Option";
7import * as Schema from "effect/Schema";
7import * as Scope from "effect/Scope";8import * as Scope from "effect/Scope";
89
9import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth";10import { clerkFrontendApiHostnameFromPublishableKey } from "@t3tools/shared/relayAuth";
...@@ -14,6 +15,32 @@ import * as DesktopEnvironment from "./DesktopEnvironment.ts";...@@ -14,6 +15,32 @@ import * as DesktopEnvironment from "./DesktopEnvironment.ts";
1415
15declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;16declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined;
1617
18export class DesktopClerkBridgeInitializationError extends Schema.TaggedErrorClass<DesktopClerkBridgeInitializationError>()(
19 "DesktopClerkBridgeInitializationError",
20 {
21 stateDir: Schema.String,
22 isDevelopment: Schema.Boolean,
23 cause: Schema.Defect(),
24 },
25) {
26 override get message(): string {
27 return `Failed to initialize the desktop Clerk bridge for state directory "${this.stateDir}" (development: ${this.isDevelopment}).`;
28 }
29}
30
31export class DesktopClerkBridgeCleanupError extends Schema.TaggedErrorClass<DesktopClerkBridgeCleanupError>()(
32 "DesktopClerkBridgeCleanupError",
33 {
34 stateDir: Schema.String,
35 isDevelopment: Schema.Boolean,
36 cause: Schema.Defect(),
37 },
38) {
39 override get message(): string {
40 return `Failed to clean up the desktop Clerk bridge for state directory "${this.stateDir}" (development: ${this.isDevelopment}).`;
41 }
42}
43
17export class DesktopClerk extends Context.Service<44export class DesktopClerk extends Context.Service<
18 DesktopClerk,45 DesktopClerk,
19 {46 {
...@@ -55,11 +82,28 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea...@@ -55,11 +82,28 @@ export function createDesktopClerkBridge(stateDir: string, isDevelopment: boolea
55 });82 });
56}83}
5784
58const make = Effect.gen(function* () {85export const make = Effect.gen(function* () {
59 const environment = yield* DesktopEnvironment.DesktopEnvironment;86 const environment = yield* DesktopEnvironment.DesktopEnvironment;
60 yield* Effect.acquireRelease(87 yield* Effect.acquireRelease(
61 Effect.sync(() => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment)),88 Effect.try({
62 (bridge) => Effect.sync(() => bridge.cleanup()),89 try: () => createDesktopClerkBridge(environment.stateDir, environment.isDevelopment),
90 catch: (cause) =>
91 new DesktopClerkBridgeInitializationError({
92 stateDir: environment.stateDir,
93 isDevelopment: environment.isDevelopment,
94 cause,
95 }),
96 }),
97 (bridge) =>
98 Effect.try({
99 try: () => bridge.cleanup(),
100 catch: (cause) =>
101 new DesktopClerkBridgeCleanupError({
102 stateDir: environment.stateDir,
103 isDevelopment: environment.isDevelopment,
104 cause,
105 }),
106 }).pipe(Effect.orDie),
63 );107 );
64108
65 return DesktopClerk.of({109 return DesktopClerk.of({