@@ -10,35 +10,40 @@ import type {
1010 BrowserImportSource ,
1111 BrowserImportUnavailableReason ,
1212} from "@t3tools/contracts" ;
13+ import { BrowserImportFailureReason } from "@t3tools/contracts" ;
1314import * as Context from "effect/Context" ;
1415import * as Effect from "effect/Effect" ;
16+ import * as FileSystem from "effect/FileSystem" ;
1517import * as Layer from "effect/Layer" ;
18+ import * as Path from "effect/Path" ;
1619import * as Schema from "effect/Schema" ;
1720
1821import { HostProcessExecutablePath , HostProcessPlatform } from "@t3tools/shared/hostProcess" ;
1922
2023import * as BrowserSession from "../BrowserSession.ts" ;
21- import {
22- ChromiumCookieReadError ,
23- readChromiumCookies ,
24- type ChromiumCookie ,
25- } from "./ChromiumCookies.ts" ;
24+ import { readChromiumCookies } from "./ChromiumCookies.ts" ;
2625import {
2726 BROWSER_IMPORT_SOURCES ,
2827 cookieDatabasePath ,
2928 isSourceInstalled ,
3029 isSourceRunning ,
3130 listSourceProfiles ,
31+ sourcePaths ,
3232 type BrowserImportSourceDefinition ,
33+ type SourcePaths ,
3334} from "./Sources.ts" ;
3435
3536export class BrowserImportFailedError extends Schema . TaggedErrorClass < BrowserImportFailedError > ( ) (
3637 "BrowserImportFailedError" ,
3738 {
3839 sourceId : Schema . String ,
39- reason : Schema . String ,
40+ reason : BrowserImportFailureReason ,
41+ /** Kept for the log; the user only ever sees the reason's copy. */
42+ cause : Schema . optional ( Schema . Defect ( ) ) ,
4043 } ,
4144) {
45+ // The reason token is part of the message on purpose: IPC flattens the error
46+ // to its message, and the renderer maps that token back to user-facing copy.
4247 override get message ( ) : string {
4348 return `Importing cookies from ${ this . sourceId } failed: ${ this . reason } .` ;
4449 }
@@ -57,36 +62,40 @@ export class BrowserImport extends Context.Service<
5762 }
5863> ( ) ( "@t3tools/desktop/preview/BrowserImport/BrowserImport" ) { }
5964
60- const unavailableReason = async (
65+ const unavailableReason = Effect . fn ( "BrowserImport.unavailableReason" ) ( function * (
6166 definition : BrowserImportSourceDefinition ,
6267 platform : NodeJS . Platform ,
63- ) : Promise < BrowserImportUnavailableReason | undefined > => {
68+ paths : SourcePaths ,
69+ ) : Effect . fn . Return < BrowserImportUnavailableReason | undefined , never , FileSystem . FileSystem > {
6470 if ( ! definition . platforms . includes ( platform ) ) return "unsupportedPlatform" ;
65- if ( ! ( await isSourceInstalled ( definition ) ) ) return "notInstalled" ;
66- if ( await isSourceRunning ( definition ) ) return "browserRunning" ;
71+ if ( ! ( yield * isSourceInstalled ( definition , paths ) ) ) return "notInstalled" ;
72+ if ( yield * isSourceRunning ( definition , paths ) ) return "browserRunning" ;
6773 return undefined ;
68- } ;
74+ } ) ;
6975
7076export const make = Effect . gen ( function * BrowserImportMake ( ) {
7177 const browserSession = yield * BrowserSession . BrowserSession ;
7278 const platform = yield * HostProcessPlatform ;
7379 const executablePath = yield * HostProcessExecutablePath ;
74-
75- const listSources = Effect . promise ( async ( ) : Promise < ReadonlyArray < BrowserImportSource > > => {
76- const sources : BrowserImportSource [ ] = [ ] ;
77- for ( const definition of BROWSER_IMPORT_SOURCES ) {
78- const unavailable = await unavailableReason ( definition , platform ) ;
79- sources . push ( {
80+ // Captured here so the service's methods stay free of a requirements
81+ // channel: the layer is built where NodeServices is already in scope.
82+ const platformServices = yield * Effect . context < FileSystem . FileSystem | Path . Path > ( ) ;
83+ const paths = yield * sourcePaths ;
84+
85+ const listSources : Effect . Effect < ReadonlyArray < BrowserImportSource > > = Effect . forEach (
86+ BROWSER_IMPORT_SOURCES ,
87+ Effect . fnUntraced ( function * ( definition ) {
88+ const unavailable = yield * unavailableReason ( definition , platform , paths ) ;
89+ return {
8090 id : definition . id ,
8191 name : definition . name ,
8292 // Listing profiles touches the source's own files, so skip it when the
8393 // source is unusable anyway.
84- profiles : unavailable === undefined ? await listSourceProfiles ( definition ) : [ ] ,
94+ profiles : unavailable === undefined ? yield * listSourceProfiles ( definition , paths ) : [ ] ,
8595 ...( unavailable === undefined ? { } : { unavailable } ) ,
86- } ) ;
87- }
88- return sources ;
89- } ) ;
96+ } satisfies BrowserImportSource ;
97+ } ) ,
98+ ) . pipe ( Effect . provide ( platformServices ) ) ;
9099
91100 const importCookies = Effect . fn ( "BrowserImport.importCookies" ) ( function * ( input : {
92101 readonly input : BrowserImportInput ;
@@ -99,11 +108,13 @@ export const make = Effect.gen(function* BrowserImportMake() {
99108 if ( ! definition ) {
100109 return yield * new BrowserImportFailedError ( {
101110 sourceId : input . input . sourceId ,
102- reason : "unknown source " ,
111+ reason : "unknownSource " ,
103112 } ) ;
104113 }
105114
106- const blocked = yield * Effect . promise ( ( ) => unavailableReason ( definition , platform ) ) ;
115+ const blocked = yield * unavailableReason ( definition , platform , paths ) . pipe (
116+ Effect . provide ( platformServices ) ,
117+ ) ;
107118 if ( blocked !== undefined ) {
108119 return yield * new BrowserImportFailedError ( { sourceId : definition . id , reason : blocked } ) ;
109120 }
@@ -120,7 +131,9 @@ export const make = Effect.gen(function* BrowserImportMake() {
120131 // source itself reported it. Forwarding it unchecked would let `..`
121132 // segments walk out of the browser's user-data directory and read any
122133 // cookie database reachable on disk.
123- const sourceProfiles = yield * Effect . promise ( ( ) => listSourceProfiles ( definition ) ) ;
134+ const sourceProfiles = yield * listSourceProfiles ( definition , paths ) . pipe (
135+ Effect . provide ( platformServices ) ,
136+ ) ;
124137 const requestedProfile = sourceProfiles . find (
125138 ( profile ) => profile . directory === input . input . sourceProfileDirectory ,
126139 ) ;
@@ -131,28 +144,30 @@ export const make = Effect.gen(function* BrowserImportMake() {
131144 } ) ;
132145 }
133146
134- const cookies : ReadonlyArray < ChromiumCookie > = yield * Effect . tryPromise ( {
135- try : ( ) =>
136- readChromiumCookies ( {
137- cookieDatabasePath : cookieDatabasePath ( definition , requestedProfile . directory ) ,
138- keychainService : definition . keychainService ,
139- keychainAccount : definition . keychainAccount ,
140- platform,
141- } ) ,
142- catch : ( cause ) =>
143- new BrowserImportFailedError ( {
144- sourceId : definition . id ,
145- reason : cause instanceof ChromiumCookieReadError ? cause . failure . reason : "readFailed" ,
146- } ) ,
147- } ) ;
147+ const cookies = yield * readChromiumCookies ( {
148+ cookieDatabasePath : cookieDatabasePath ( definition , paths , requestedProfile . directory ) ,
149+ keychainService : definition . keychainService ,
150+ keychainAccount : definition . keychainAccount ,
151+ platform,
152+ } ) . pipe (
153+ Effect . scoped ,
154+ Effect . provide ( platformServices ) ,
155+ Effect . mapError (
156+ ( cause ) =>
157+ new BrowserImportFailedError ( { sourceId : definition . id , reason : cause . reason , cause } ) ,
158+ ) ,
159+ ) ;
148160
149- const session = yield * browserSession
150- . getSession ( input . scope , input . persistent )
151- . pipe (
152- Effect . mapError (
153- ( ) => new BrowserImportFailedError ( { sourceId : definition . id , reason : "readFailed" } ) ,
154- ) ,
155- ) ;
161+ const session = yield * browserSession . getSession ( input . scope , input . persistent ) . pipe (
162+ Effect . mapError (
163+ ( cause ) =>
164+ new BrowserImportFailedError ( {
165+ sourceId : definition . id ,
166+ reason : "sessionUnavailable" ,
167+ cause,
168+ } ) ,
169+ ) ,
170+ ) ;
156171
157172 // Written one at a time rather than in parallel: Chromium's cookie store
158173 // serialises writes anyway, and a rejected cookie should only cost itself.
0 commit comments