Skip to content

Commit 3343565

Browse files
refactor(desktop): give the Firefox read path a tagged error
The Firefox branch failed with an anonymous `{ reason, cause }` literal, and typing the shared `read` value as that shape erased `ChromiumCookieReadError`'s tag from the error channel — neither branch could then be handled with `Effect.catchTags`. `FirefoxCookieReadError` mirrors its Chromium counterpart, so the union stays structurally identifiable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 1b132e4 commit 3343565

2 files changed

Lines changed: 46 additions & 19 deletions

File tree

apps/desktop/src/preview/BrowserImport/BrowserImport.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import * as Scope from "effect/Scope";
2222
import { HostProcessExecutablePath, HostProcessPlatform } from "@t3tools/shared/hostProcess";
2323

2424
import * as BrowserSession from "../BrowserSession.ts";
25-
import { readChromiumCookies } from "./ChromiumCookies.ts";
25+
import { ChromiumCookieReadError, readChromiumCookies } from "./ChromiumCookies.ts";
2626
import type { ImportedCookie } from "./CookieDatabase.ts";
27-
import { readFirefoxCookies } from "./FirefoxCookies.ts";
27+
import { FirefoxCookieReadError, readFirefoxCookies } from "./FirefoxCookies.ts";
2828
import {
2929
BROWSER_IMPORT_SOURCES,
3030
cookieDatabasePath,
@@ -98,7 +98,8 @@ export const make = Effect.gen(function* BrowserImportMake() {
9898
name: definition.name,
9999
// Listing profiles touches the source's own files, so skip it when the
100100
// source is unusable anyway.
101-
profiles: unavailable === undefined ? yield* listSourceProfiles(definition, pathContext) : [],
101+
profiles:
102+
unavailable === undefined ? yield* listSourceProfiles(definition, pathContext) : [],
102103
...(unavailable === undefined ? {} : { unavailable }),
103104
} satisfies BrowserImportSource;
104105
}),
@@ -159,23 +160,23 @@ export const make = Effect.gen(function* BrowserImportMake() {
159160
});
160161
}
161162

163+
// Both branches fail with a tagged error carrying a `reason`, so the union
164+
// stays structurally identifiable rather than collapsing to an anonymous
165+
// shape that `Effect.catchTags` could not tell apart.
162166
const read: Effect.Effect<
163167
ReadonlyArray<ImportedCookie>,
164-
{ readonly reason: BrowserImportFailureReason },
168+
ChromiumCookieReadError | FirefoxCookieReadError,
165169
FileSystem.FileSystem | Path.Path | Scope.Scope
166-
> =
167-
definition.engine === "firefox"
168-
? readFirefoxCookies(databasePath).pipe(
169-
Effect.mapError((cause) => ({ reason: "readFailed" as const, cause })),
170-
)
171-
: readChromiumCookies({
172-
cookieDatabasePath: databasePath,
173-
// Only reached on macOS: `unavailableReason` rejects Chromium
174-
// elsewhere until those key stores are implemented.
175-
keychainService: definition.keychainService ?? "",
176-
keychainAccount: definition.keychainAccount ?? "",
177-
platform,
178-
});
170+
> = definition.engine === "firefox"
171+
? readFirefoxCookies(databasePath)
172+
: readChromiumCookies({
173+
cookieDatabasePath: databasePath,
174+
// Only reached on macOS: `unavailableReason` rejects Chromium
175+
// elsewhere until those key stores are implemented.
176+
keychainService: definition.keychainService ?? "",
177+
keychainAccount: definition.keychainAccount ?? "",
178+
platform,
179+
});
179180

180181
const cookies = yield* read.pipe(
181182
Effect.scoped,

apps/desktop/src/preview/BrowserImport/FirefoxCookies.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,27 @@ import { snapshotCookieDatabase, type ImportedCookie } from "./CookieDatabase.ts
2222
* that is the modern default, and guessing "none" would widen a cookie's scope
2323
* on import.
2424
*/
25+
export const FirefoxCookieReadReason = Schema.Literals(["readFailed"]);
26+
export type FirefoxCookieReadReason = typeof FirefoxCookieReadReason.Type;
27+
28+
/**
29+
* Mirrors `ChromiumCookieReadError` so both engines fail with a tagged error
30+
* the service can tell apart, rather than one of them widening the channel to
31+
* an anonymous shape.
32+
*/
33+
export class FirefoxCookieReadError extends Schema.TaggedErrorClass<FirefoxCookieReadError>()(
34+
"FirefoxCookieReadError",
35+
{
36+
reason: FirefoxCookieReadReason,
37+
/** Kept for the log; never surfaced to the user. */
38+
cause: Schema.optional(Schema.Defect()),
39+
},
40+
) {
41+
override get message(): string {
42+
return `Could not read Firefox cookies: ${this.reason}.`;
43+
}
44+
}
45+
2546
const sameSiteFromColumn = (value: number): ImportedCookie["sameSite"] => {
2647
if (value === 0) return "no_restriction";
2748
if (value === 2) return "strict";
@@ -45,7 +66,9 @@ const decodeCookieRows = Schema.decodeUnknownEffect(Schema.Array(CookieRow));
4566
export const readFirefoxCookies = Effect.fn("FirefoxCookies.readFirefoxCookies")(function* (
4667
cookieDatabasePath: string,
4768
) {
48-
const snapshotPath = yield* snapshotCookieDatabase(cookieDatabasePath);
69+
const snapshotPath = yield* snapshotCookieDatabase(cookieDatabasePath).pipe(
70+
Effect.mapError((cause) => new FirefoxCookieReadError({ reason: "readFailed", cause })),
71+
);
4972

5073
const rows = yield* Effect.gen(function* () {
5174
const sql = yield* SqlClient.SqlClient;
@@ -60,7 +83,10 @@ export const readFirefoxCookies = Effect.fn("FirefoxCookies.readFirefoxCookies")
6083
where originAttributes = ''
6184
`;
6285
return yield* decodeCookieRows(raw);
63-
}).pipe(Effect.provide(NodeSqliteClient.layer({ filename: snapshotPath, readonly: true })));
86+
}).pipe(
87+
Effect.provide(NodeSqliteClient.layer({ filename: snapshotPath, readonly: true })),
88+
Effect.mapError((cause) => new FirefoxCookieReadError({ reason: "readFailed", cause })),
89+
);
6490

6591
return rows.map((row) => {
6692
const secure = row.isSecure === 1;

0 commit comments

Comments
 (0)