@@ -12,6 +12,8 @@ import type {
1212 RawAuditLogEventT ,
1313 CategoryNotes ,
1414 AuditLogConfig ,
15+ DeduplicatedAuditLogEntry ,
16+ AuditLogVersionIndex ,
1517} from '../types'
1618import config from './config.json'
1719
@@ -21,6 +23,86 @@ export const AUDIT_LOG_DATA_DIR = 'src/audit-logs/data'
2123const auditLogEventsCache = new Map < string , Map < string , AuditLogEventT [ ] > > ( )
2224const categorizedAuditLogEventsCache = new Map < string , Map < string , CategorizedEvents > > ( )
2325
26+ // Shared dedup data — loaded once, shared across all versions
27+ let sharedEntries : DeduplicatedAuditLogEntry [ ] | null = null
28+ let sharedFieldsPool : string [ ] [ ] | null = null
29+ let sharedVersionIndex : AuditLogVersionIndex | null = null
30+ let sharedFormatAvailable : boolean | null = null // null = not checked yet
31+
32+ // A missing shared-format file is expected (per-version files are the fallback),
33+ // but a corrupt or unparseable file should fail loudly rather than silently
34+ // degrade to the per-version files and hide bad generated data.
35+ function isFileNotFoundError ( err : unknown ) : boolean {
36+ if ( ! ( err instanceof Error ) || ! ( 'code' in err ) ) return false
37+ const code = ( err as NodeJS . ErrnoException ) . code
38+ return code === 'ENOENT' || code === 'ENOTDIR'
39+ }
40+
41+ function loadSharedFormat ( ) : boolean {
42+ if ( sharedFormatAvailable !== null ) return sharedFormatAvailable
43+ try {
44+ sharedEntries = readCompressedJsonFileFallback (
45+ path . join ( AUDIT_LOG_DATA_DIR , 'shared' , 'entries.json' ) ,
46+ ) as DeduplicatedAuditLogEntry [ ]
47+ sharedFieldsPool = readCompressedJsonFileFallback (
48+ path . join ( AUDIT_LOG_DATA_DIR , 'shared' , 'fields-pool.json' ) ,
49+ ) as string [ ] [ ]
50+ sharedVersionIndex = readCompressedJsonFileFallback (
51+ path . join ( AUDIT_LOG_DATA_DIR , 'version-index.json' ) ,
52+ ) as AuditLogVersionIndex
53+ // Freeze pool data so reconstructed events (which return references into
54+ // these pools) can't be mutated by downstream code and leak across versions.
55+ Object . freeze ( sharedEntries )
56+ Object . freeze ( sharedFieldsPool )
57+ for ( const fields of sharedFieldsPool ) Object . freeze ( fields )
58+ sharedFormatAvailable = true
59+ } catch ( err ) {
60+ if ( isFileNotFoundError ( err ) ) {
61+ // Shared files don't exist — fall back to per-version files silently.
62+ sharedFormatAvailable = false
63+ } else {
64+ // Corrupt JSON, schema mismatch, etc. — surface this instead of hiding it.
65+ console . error ( 'Failed to load shared audit log dedup format (corrupt data?):' , err )
66+ throw err
67+ }
68+ }
69+ return sharedFormatAvailable
70+ }
71+
72+ function reconstructEventsFromSharedFormat ( version : string , page : string ) : AuditLogEventT [ ] | null {
73+ if ( ! loadSharedFormat ( ) ) return null
74+ const indices = sharedVersionIndex ?. [ version ] ?. [ page ]
75+ if ( ! indices ) return null
76+
77+ return indices . map ( ( idx ) => {
78+ if ( idx < 0 || idx >= sharedEntries ! . length ) {
79+ throw new RangeError (
80+ `Audit log version-index references entry ${ idx } for ${ version } /${ page } , ` +
81+ `but the entries pool only has ${ sharedEntries ! . length } entries. ` +
82+ `The shared dedup data may be stale or corrupt.` ,
83+ )
84+ }
85+ const entry = sharedEntries ! [ idx ]
86+ const event : AuditLogEventT = {
87+ action : entry . action ,
88+ description : entry . description ,
89+ }
90+ if ( entry . docs_reference_links ) event . docs_reference_links = entry . docs_reference_links
91+ if ( entry . docs_reference_titles ) event . docs_reference_titles = entry . docs_reference_titles
92+ if ( entry . fieldsIndex !== undefined ) {
93+ if ( entry . fieldsIndex < 0 || entry . fieldsIndex >= sharedFieldsPool ! . length ) {
94+ throw new RangeError (
95+ `Audit log entry references fields index ${ entry . fieldsIndex } for ${ version } /${ page } , ` +
96+ `but the fields pool only has ${ sharedFieldsPool ! . length } entries. ` +
97+ `The shared dedup data may be stale or corrupt.` ,
98+ )
99+ }
100+ event . fields = sharedFieldsPool ! [ entry . fieldsIndex ]
101+ }
102+ return event
103+ } )
104+ }
105+
24106type PipelineConfig = {
25107 sha : string
26108 appendedDescriptions : Record < string , string >
@@ -169,21 +251,24 @@ async function resolveReferenceLinksToTitles(
169251// ]
170252export function getAuditLogEvents ( page : string , version : string ) : AuditLogEventT [ ] {
171253 const openApiVersion = getOpenApiVersion ( version )
172- const auditLogFileName = path . join ( AUDIT_LOG_DATA_DIR , openApiVersion , `${ page } .json` )
173254
174255 // If the data isn't cached for an entire version or a particular page, read
175- // the data from the JSON file the first time around
256+ // the data from the shared dedup format or fall back to per-version JSON files
176257 if ( ! auditLogEventsCache . has ( openApiVersion ) ) {
177258 auditLogEventsCache . set ( openApiVersion , new Map ( ) )
178- auditLogEventsCache . get ( openApiVersion ) ?. set ( page , [ ] )
179- auditLogEventsCache
180- . get ( openApiVersion )
181- ?. set ( page , readCompressedJsonFileFallback ( auditLogFileName ) as AuditLogEventT [ ] )
182- } else if ( ! auditLogEventsCache . get ( openApiVersion ) ?. has ( page ) ) {
183- auditLogEventsCache . get ( openApiVersion ) ?. set ( page , [ ] )
184- auditLogEventsCache
185- . get ( openApiVersion )
186- ?. set ( page , readCompressedJsonFileFallback ( auditLogFileName ) as AuditLogEventT [ ] )
259+ }
260+ if ( ! auditLogEventsCache . get ( openApiVersion ) ?. has ( page ) ) {
261+ // Try shared deduplicated format first
262+ const events = reconstructEventsFromSharedFormat ( openApiVersion , page )
263+ if ( events ) {
264+ auditLogEventsCache . get ( openApiVersion ) ?. set ( page , events )
265+ } else {
266+ // Fall back to per-version JSON file
267+ const auditLogFileName = path . join ( AUDIT_LOG_DATA_DIR , openApiVersion , `${ page } .json` )
268+ auditLogEventsCache
269+ . get ( openApiVersion )
270+ ?. set ( page , readCompressedJsonFileFallback ( auditLogFileName ) as AuditLogEventT [ ] )
271+ }
187272 }
188273
189274 const auditLogEvents = auditLogEventsCache . get ( openApiVersion ) ?. get ( page )
0 commit comments