@@ -2,18 +2,40 @@ import { randomUUID } from 'node:crypto';
22import { mkdir , readFile , rename , rm , writeFile } from 'node:fs/promises' ;
33import path from 'node:path' ;
44
5- const fmtCacheFileName = 'v1 .json' ;
6- const fmtCacheVersion = 1 ;
5+ const fmtCacheFileName = 'cache .json' ;
6+ const fmtCacheVersion = 2 ;
77
8- type FmtCacheState = 'clean' | 'dirty' | 'unsupported' ;
9- type FmtCacheEntry =
10- | readonly [ contentHash : string , optionsHash : string , state : 'clean' | 'dirty' ]
11- | readonly [ contentHash : string | null , optionsHash : string , state : 'unsupported' ] ;
8+ const fileEntryWidth = 4 ;
9+ const contentHashOffset = 1 ;
10+ const optionsIndexOffset = 2 ;
11+ const stateOffset = 3 ;
12+
13+ const fmtCacheStates = [ 'clean' , 'dirty' , 'unsupported' ] as const ;
14+ type FmtCacheState = ( typeof fmtCacheStates ) [ number ] ;
15+ type FmtCacheStateId = 0 | 1 | 2 ;
16+
17+ const fmtCacheStateIds = {
18+ clean : 0 ,
19+ dirty : 1 ,
20+ unsupported : 2 ,
21+ } as const satisfies Record < FmtCacheState , FmtCacheStateId > ;
22+
23+ type FmtCacheFileValue = string | number ;
24+ type FmtCacheEntry = readonly [ contentHash : string , optionsHash : string , state : FmtCacheState ] ;
1225
1326interface FmtCacheFile {
1427 version : typeof fmtCacheVersion ;
1528 namespace : string ;
16- files : Record < string , FmtCacheEntry > ;
29+ options : string [ ] ;
30+ /** Repeated tuples of file path, content hash, options index, and numeric state. */
31+ files : FmtCacheFileValue [ ] ;
32+ }
33+
34+ interface ParsedFmtCacheFile {
35+ cache : FmtCacheFile ;
36+ fileOffsets : Map < string , number > ;
37+ optionsIndexes : Map < string , number > ;
38+ optionsUseCounts : number [ ] ;
1739}
1840
1941interface FmtCacheStore {
@@ -23,66 +45,61 @@ interface FmtCacheStore {
2345 save ( ) : Promise < boolean > ;
2446}
2547
26- const createEmptyCache = ( namespace : string ) : FmtCacheFile => ( {
27- version : fmtCacheVersion ,
28- namespace,
29- files : Object . create ( null ) as Record < string , FmtCacheEntry > ,
48+ const createEmptyCache = ( namespace : string ) : ParsedFmtCacheFile => ( {
49+ cache : {
50+ version : fmtCacheVersion ,
51+ namespace,
52+ options : [ ] ,
53+ files : [ ] ,
54+ } ,
55+ fileOffsets : new Map ( ) ,
56+ optionsIndexes : new Map ( ) ,
57+ optionsUseCounts : [ ] ,
3058} ) ;
3159
32- const parseCacheEntry = ( value : unknown ) : FmtCacheEntry | undefined => {
33- if ( ! Array . isArray ( value ) || value . length !== 3 || typeof value [ 1 ] !== 'string' ) {
34- return ;
35- }
36-
37- if ( value [ 2 ] === 'unsupported' ) {
38- return value [ 0 ] === null || typeof value [ 0 ] === 'string'
39- ? [ value [ 0 ] , value [ 1 ] , value [ 2 ] ]
40- : undefined ;
41- }
42- if ( typeof value [ 0 ] !== 'string' || ( value [ 2 ] !== 'clean' && value [ 2 ] !== 'dirty' ) ) {
43- return ;
44- }
45-
46- return [ value [ 0 ] , value [ 1 ] , value [ 2 ] ] ;
47- } ;
48-
49- const parseCacheFile = ( content : string ) : FmtCacheFile | undefined => {
60+ const parseCacheFile = ( content : string ) : ParsedFmtCacheFile | undefined => {
5061 let value : unknown ;
5162 try {
5263 value = JSON . parse ( content ) ;
5364 } catch {
5465 return ;
5566 }
5667
68+ if ( typeof value !== 'object' || value === null || Array . isArray ( value ) ) {
69+ return ;
70+ }
71+
72+ const cache = value as FmtCacheFile ;
73+ const { version, namespace, options, files } = cache ;
5774 if (
58- typeof value !== 'object' ||
59- value === null ||
60- Array . isArray ( value ) ||
61- ! ( 'version' in value ) ||
62- value . version !== fmtCacheVersion ||
63- ! ( 'namespace' in value ) ||
64- typeof value . namespace !== 'string' ||
65- ! ( 'files' in value ) ||
66- typeof value . files !== 'object' ||
67- value . files === null ||
68- Array . isArray ( value . files )
75+ version !== fmtCacheVersion ||
76+ typeof namespace !== 'string' ||
77+ ! Array . isArray ( options ) ||
78+ ! Array . isArray ( files ) ||
79+ files . length % fileEntryWidth !== 0
6980 ) {
7081 return ;
7182 }
7283
73- const files = Object . create ( null ) as Record < string , FmtCacheEntry > ;
74- for ( const [ filePath , rawEntry ] of Object . entries ( value . files ) ) {
75- const entry = parseCacheEntry ( rawEntry ) ;
76- if ( ! entry ) {
77- return ;
78- }
79- files [ filePath ] = entry ;
84+ const optionsIndexes = new Map < string , number > ( ) ;
85+ for ( let index = 0 ; index < options . length ; index ++ ) {
86+ optionsIndexes . set ( options [ index ] , index ) ;
87+ }
88+
89+ const fileOffsets = new Map < string , number > ( ) ;
90+ const optionsUseCounts = new Array < number > ( options . length ) . fill ( 0 ) ;
91+ for ( let offset = 0 ; offset < files . length ; offset += fileEntryWidth ) {
92+ const filePath = files [ offset ] as string ;
93+ const optionsIndex = files [ offset + optionsIndexOffset ] as number ;
94+ fileOffsets . set ( filePath , offset ) ;
95+ optionsUseCounts [ optionsIndex ] ++ ;
8096 }
8197
8298 return {
83- version : fmtCacheVersion ,
84- namespace : value . namespace ,
85- files,
99+ cache,
100+ fileOffsets,
101+ optionsIndexes,
102+ optionsUseCounts,
86103 } ;
87104} ;
88105
@@ -100,43 +117,124 @@ const getTemporaryPath = (filePath: string): string =>
100117class FmtCacheStoreImpl implements FmtCacheStore {
101118 readonly #filePath: string ;
102119 readonly #cache: FmtCacheFile ;
120+ readonly #fileOffsets: Map < string , number > ;
121+ readonly #optionsIndexes: Map < string , number > ;
122+ readonly #optionsUseCounts: number [ ] ;
103123 #savedContent: string | undefined ;
104124 #changed: boolean ;
105125
106126 constructor (
107127 filePath : string ,
108- cache : FmtCacheFile ,
128+ parsed : ParsedFmtCacheFile ,
109129 savedContent : string | undefined ,
110130 changed : boolean ,
111131 ) {
112132 this . #filePath = filePath ;
113- this . #cache = cache ;
133+ this . #cache = parsed . cache ;
134+ this . #fileOffsets = parsed . fileOffsets ;
135+ this . #optionsIndexes = parsed . optionsIndexes ;
136+ this . #optionsUseCounts = parsed . optionsUseCounts ;
114137 this . #savedContent = savedContent ;
115138 this . #changed = changed ;
116139 }
117140
118141 get ( filePath : string ) : FmtCacheEntry | undefined {
119- return this . #cache. files [ filePath ] ;
142+ const offset = this . #fileOffsets. get ( filePath ) ;
143+ if ( offset === undefined ) {
144+ return ;
145+ }
146+
147+ const { files, options } = this . #cache;
148+ const contentHash = files [ offset + contentHashOffset ] as string ;
149+ const optionsHash = options [ files [ offset + optionsIndexOffset ] as number ] ;
150+ const state = fmtCacheStates [ files [ offset + stateOffset ] as FmtCacheStateId ] ;
151+ return [ contentHash , optionsHash , state ] ;
120152 }
121153
122154 set ( filePath : string , entry : FmtCacheEntry ) : void {
123- const current = this . #cache. files [ filePath ] ;
124- if ( current ?. [ 0 ] === entry [ 0 ] && current [ 1 ] === entry [ 1 ] && current [ 2 ] === entry [ 2 ] ) {
125- return ;
155+ const { files, options } = this . #cache;
156+ const [ contentHash , optionsHash , state ] = entry ;
157+ const stateId = fmtCacheStateIds [ state ] ;
158+ const offset = this . #fileOffsets. get ( filePath ) ;
159+
160+ if ( offset !== undefined ) {
161+ const currentOptionsIndex = files [ offset + optionsIndexOffset ] as number ;
162+ if (
163+ files [ offset + contentHashOffset ] === contentHash &&
164+ options [ currentOptionsIndex ] === optionsHash &&
165+ files [ offset + stateOffset ] === stateId
166+ ) {
167+ return ;
168+ }
169+
170+ const optionsIndex = this . #getOrCreateOptionsIndex( optionsHash ) ;
171+ if ( currentOptionsIndex !== optionsIndex ) {
172+ this . #optionsUseCounts[ currentOptionsIndex ] -- ;
173+ this . #optionsUseCounts[ optionsIndex ] ++ ;
174+ files [ offset + optionsIndexOffset ] = optionsIndex ;
175+ }
176+ files [ offset + contentHashOffset ] = contentHash ;
177+ files [ offset + stateOffset ] = stateId ;
178+ } else {
179+ const optionsIndex = this . #getOrCreateOptionsIndex( optionsHash ) ;
180+ const nextOffset = files . length ;
181+ files . push ( filePath , contentHash , optionsIndex , stateId ) ;
182+ this . #fileOffsets. set ( filePath , nextOffset ) ;
183+ this . #optionsUseCounts[ optionsIndex ] ++ ;
126184 }
127185
128- this . #cache. files [ filePath ] =
129- entry [ 2 ] === 'unsupported'
130- ? [ entry [ 0 ] , entry [ 1 ] , 'unsupported' ]
131- : [ entry [ 0 ] , entry [ 1 ] , entry [ 2 ] ] ;
132186 this . #changed = true ;
133187 }
134188
189+ #getOrCreateOptionsIndex( optionsHash : string ) : number {
190+ const current = this . #optionsIndexes. get ( optionsHash ) ;
191+ if ( current !== undefined ) {
192+ return current ;
193+ }
194+
195+ const index = this . #cache. options . length ;
196+ this . #cache. options . push ( optionsHash ) ;
197+ this . #optionsIndexes. set ( optionsHash , index ) ;
198+ this . #optionsUseCounts. push ( 0 ) ;
199+ return index ;
200+ }
201+
202+ #compactUnusedOptions( ) : void {
203+ if ( ! this . #optionsUseCounts. includes ( 0 ) ) {
204+ return ;
205+ }
206+
207+ const { files, options } = this . #cache;
208+ const nextOptions : string [ ] = [ ] ;
209+ const nextUseCounts : number [ ] = [ ] ;
210+ const remappedIndexes = new Int32Array ( options . length ) . fill ( - 1 ) ;
211+ for ( let index = 0 ; index < options . length ; index ++ ) {
212+ const useCount = this . #optionsUseCounts[ index ] ;
213+ if ( useCount > 0 ) {
214+ remappedIndexes [ index ] = nextOptions . length ;
215+ nextOptions . push ( options [ index ] ) ;
216+ nextUseCounts . push ( useCount ) ;
217+ }
218+ }
219+ for ( let offset = 0 ; offset < files . length ; offset += fileEntryWidth ) {
220+ const currentIndex = files [ offset + optionsIndexOffset ] as number ;
221+ files [ offset + optionsIndexOffset ] = remappedIndexes [ currentIndex ] ;
222+ }
223+
224+ options . splice ( 0 , options . length , ...nextOptions ) ;
225+ this . #optionsUseCounts. splice ( 0 , this . #optionsUseCounts. length , ...nextUseCounts ) ;
226+ this . #optionsIndexes. clear ( ) ;
227+ for ( let index = 0 ; index < options . length ; index ++ ) {
228+ this . #optionsIndexes. set ( options [ index ] , index ) ;
229+ }
230+ }
231+
135232 async save ( ) : Promise < boolean > {
136233 if ( ! this . #changed) {
137234 return false ;
138235 }
139236
237+ this . #compactUnusedOptions( ) ;
140238 const content = serializeCache ( this . #cache) ;
141239 if ( content === this . #savedContent) {
142240 this . #changed = false ;
@@ -164,13 +262,13 @@ const loadFmtCacheStore = async (filePath: string, namespace: string): Promise<F
164262
165263 try {
166264 const content = await readFile ( filePath , 'utf8' ) ;
167- const cache = parseCacheFile ( content ) ;
168- if ( ! cache ) {
265+ const parsed = parseCacheFile ( content ) ;
266+ if ( ! parsed ) {
169267 return new FmtCacheStoreImpl ( filePath , emptyCache , undefined , true ) ;
170268 }
171269
172- return cache . namespace === namespace
173- ? new FmtCacheStoreImpl ( filePath , cache , content , false )
270+ return parsed . cache . namespace === namespace
271+ ? new FmtCacheStoreImpl ( filePath , parsed , content , false )
174272 : new FmtCacheStoreImpl ( filePath , emptyCache , undefined , true ) ;
175273 } catch ( error ) {
176274 const missing = isFileNotFoundError ( error ) ;
0 commit comments