1- import fs from "fs/promises"
21import path from "path"
32import {
43 FileFinder ,
@@ -12,10 +11,9 @@ import {
1211} from "@ff-labs/fff-bun"
1312import z from "zod"
1413import { Global } from "../global"
15- import { Instance } from "../project/instance"
16- import { Filesystem } from "../util/filesystem"
17- import { Glob } from "../util/glob"
18- import { Log } from "../util/log"
14+ import { Glob } from "@opencode-ai/shared/util/glob"
15+ import { Filesystem , Log } from "../util"
16+ import { registerDisposer } from "../effect/instance-registry"
1917
2018export namespace Fff {
2119 export const Match = z . object ( {
@@ -38,71 +36,95 @@ export namespace Fff {
3836 ) ,
3937 } )
4038
41- const state = Instance . state (
42- async ( ) => ( {
43- map : new Map < string , FileFinder > ( ) ,
44- pending : new Map < string , Promise < FileFinder > > ( ) ,
45- } ) ,
46- async ( state ) => {
47- for ( const pick of state . map . values ( ) ) pick . destroy ( )
48- } ,
49- )
39+ const state = {
40+ map : new Map < string , FileFinder > ( ) ,
41+ // keep the state of the already indexed fff pickers
42+ // to avoid asking if it is finished scanned every time
43+ ready : new Set < string > ( ) ,
44+ }
45+
46+ registerDisposer ( async ( directory ) => {
47+ const dir = Filesystem . resolve ( directory )
48+ const pick = state . map . get ( dir )
49+ if ( ! pick ) return
50+ state . map . delete ( dir )
51+ state . ready . delete ( dir )
52+
53+ try {
54+ pick . destroy ( )
55+ } catch { }
56+ } )
5057
5158 const root = path . join ( Global . Path . cache , "fff" )
5259
53- async function dbs ( ) {
54- await fs . mkdir ( root , { recursive : true } )
55- // fff databases are global across the file system
60+ function key ( dir : string ) {
61+ return Buffer . from ( dir ) . toString ( "base64url" )
62+ }
63+
64+ function dbs ( dir : string ) {
65+ const id = key ( dir )
5666 return {
57- frecency : path . join ( root , " frecency.mdb" ) ,
58- history : path . join ( root , " history.mdb" ) ,
67+ frecency : path . join ( root , ` ${ id } . frecency.mdb` ) ,
68+ history : path . join ( root , ` ${ id } . history.mdb` ) ,
5969 }
6070 }
6171
62- export async function picker ( cwd : string ) {
72+ export function picker ( cwd : string ) {
6373 const dir = Filesystem . resolve ( cwd )
64- const memo = await state ( )
65- const cached = memo . map . get ( dir )
74+ const cached = state . map . get ( dir )
6675 if ( cached ) return cached
6776
68- const wait = memo . pending . get ( dir )
69- if ( wait ) return wait
77+ const files = dbs ( dir )
78+ const base = Log . file ( )
79+ const logfile = path . join ( Global . Path . log , base ? "fff-" + path . basename ( base ) : "fff.log" )
80+ const result = FileFinder . create ( {
81+ aiMode : true ,
82+ basePath : dir ,
83+ frecencyDbPath : files . frecency ,
84+ historyDbPath : files . history ,
85+ logFilePath : logfile ,
86+ // fff uses the same log level
87+ logLevel : Log . currentLevel ( ) . toLowerCase ( ) as "debug" | "info" | "warn" | "error" ,
88+ // if there is second project opened within the same sesion - disable
89+ // viertual memory mapping, the memory mapping address space is finite, so we
90+ // don't want to blow user's computer (the limit depends on repo size)
91+ cacheBudgetMaxFiles : state . map . size > 0 ? 0 : undefined ,
92+ } )
93+
94+ if ( ! result . ok ) throw new Error ( result . error )
95+ const pick = result . value
96+ state . map . set ( dir , pick )
97+ return pick
98+ }
7099
71- const next = ( async ( ) => {
72- const files = await dbs ( )
73- const base = Log . file ( )
74- const logfile = path . join ( Global . Path . log , base ? "fff-" + path . basename ( base ) : "fff.log" )
75- const result = FileFinder . create ( {
76- aiMode : true ,
77- basePath : dir ,
78- frecencyDbPath : files . frecency ,
79- historyDbPath : files . history ,
80- logFilePath : logfile ,
81- logLevel : Log . currentLevel ( ) . toLowerCase ( ) as "debug" | "info" | "warn" | "error" ,
82- // if there is second project opened within the same sesion - disable
83- // content mapping, the memory mapping address space is finite, so we
84- // don't want to blow user's computer (the limit depends on repo size)
85- cacheBudgetMaxFiles : memo . map . size > 0 ? 0 : undefined ,
86- } )
87- if ( ! result . ok ) throw new Error ( result . error )
88- // we do not syncrhnously wait for the results here to not block anything
89- // fff will do the indexing in the background and will automatically
90- // become available
91- const pick = result . value
92- memo . map . set ( dir , pick )
93- return pick
94- } ) ( )
100+ const FFF_WAIT_INTERVAL = 25
101+ async function waitScan ( picker : FileFinder , timeoutMs : number ) {
102+ const start = Date . now ( )
95103
96- memo . pending . set ( dir , next )
97- try {
98- return await next
99- } finally {
100- if ( memo . pending . get ( dir ) === next ) memo . pending . delete ( dir )
104+ // becuase fff is a native library it doesn't touches event loop, so
105+ // poll for picker to be ready for returning the data if it is still scanning
106+ while ( picker . isScanning ( ) ) {
107+ if ( Date . now ( ) - start >= timeoutMs ) throw new Error ( "fff scan timeout" )
108+ await new Promise < void > ( ( resolve ) => setTimeout ( resolve , FFF_WAIT_INTERVAL ) )
109+ }
110+ }
111+
112+ async function open ( cwd : string ) {
113+ const dir = Filesystem . resolve ( cwd )
114+ const pick = picker ( cwd )
115+
116+ if ( ! state . ready . has ( dir ) ) {
117+ await waitScan ( pick , 5000 )
118+ state . ready . add ( dir )
119+ } else {
120+ pick . scanFiles ( )
121+ if ( pick . isScanning ( ) ) await waitScan ( pick , 5000 )
101122 }
123+ return pick
102124 }
103125
104126 export async function files ( input : { cwd : string ; query : string ; page ?: number ; size ?: number ; current ?: string } ) {
105- const fff = await picker ( input . cwd )
127+ const fff = await open ( input . cwd )
106128 const out = fff . fileSearch ( input . query , {
107129 pageIndex : input . page ?? 0 ,
108130 pageSize : input . size ?? 100 ,
@@ -113,7 +135,7 @@ export namespace Fff {
113135 }
114136
115137 export async function mixed ( input : { cwd : string ; query : string ; page ?: number ; size ?: number ; current ?: string } ) {
116- const fff = await picker ( input . cwd )
138+ const fff = await open ( input . cwd )
117139 const out = fff . mixedSearch ( input . query , {
118140 pageIndex : input . page ?? 0 ,
119141 pageSize : input . size ?? 100 ,
@@ -133,7 +155,7 @@ export namespace Fff {
133155 budget ?: number
134156 cursor ?: GrepCursor | null
135157 } ) {
136- const pick = await picker ( input . cwd )
158+ const pick = await open ( input . cwd )
137159 const out = pick . grep ( input . query , {
138160 mode : input . mode ,
139161 maxMatchesPerFile : input . max ,
0 commit comments