Skip to content

Commit ef59a49

Browse files
committed
fix web not-found cache consistency
1 parent b7a8f6d commit ef59a49

5 files changed

Lines changed: 128 additions & 62 deletions

File tree

web/scripts/overloads.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,53 @@
11
import assert from 'node:assert/strict';
22
import test from 'node:test';
3+
import {
4+
decodeApiFileCacheEntry,
5+
encodeApiFileCacheEntry,
6+
} from '../src/utils/cache/apiFileEntry.ts';
37
import { hasConcurrentOverloads } from '../src/views/home/overloads.ts';
48

9+
const emptyFile = {
10+
package: '',
11+
imports: [],
12+
structs: [],
13+
};
14+
15+
test('persists the source-not-found marker with the parsed API file', () => {
16+
const entry = {
17+
file: emptyFile,
18+
sourceFileNotFound: true,
19+
};
20+
21+
assert.deepEqual(
22+
decodeApiFileCacheEntry(encodeApiFileCacheEntry(entry)),
23+
entry,
24+
);
25+
});
26+
27+
test('does not deduplicate a missing source with a valid empty source', () => {
28+
const missing = encodeApiFileCacheEntry({
29+
file: emptyFile,
30+
sourceFileNotFound: true,
31+
});
32+
const validEmpty = encodeApiFileCacheEntry({
33+
file: emptyFile,
34+
sourceFileNotFound: false,
35+
});
36+
37+
assert.notDeepEqual(missing, validEmpty);
38+
assert.equal(decodeApiFileCacheEntry(missing).sourceFileNotFound, true);
39+
assert.equal(decodeApiFileCacheEntry(validEmpty).sourceFileNotFound, false);
40+
});
41+
42+
test('rejects the legacy split cache record without a not-found marker', () => {
43+
const legacyBytes = new TextEncoder().encode(JSON.stringify(emptyFile));
44+
45+
assert.throws(
46+
() => decodeApiFileCacheEntry(legacyBytes),
47+
/Cached API file entry is invalid/,
48+
);
49+
});
50+
551
test('does not treat signatures from different tags as overloads', () => {
652
assert.equal(
753
hasConcurrentOverloads([

web/src/store.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
check404File,
3-
getOrSetApiFileCache,
4-
persistentFetch,
5-
} from '@/utils/cache';
1+
import { getOrSetApiFileCache, persistentFetch } from '@/utils/cache';
62
import type { ApiFile } from '@android-cs/api-parser';
73
import {
84
loadAidlJavaFiles,
@@ -87,7 +83,7 @@ export const pullApiFileByUrl = async (
8783
if (temp) return temp;
8884
const url = getMirrorContentUrl(filePath);
8985
let parsedSourceKey: string | undefined;
90-
const file = await getOrSetApiFileCache(filePath, async () => {
86+
const entry = await getOrSetApiFileCache(filePath, async () => {
9187
const text = await limit(() => {
9288
if (signal.signal.aborted) {
9389
throw new Error('aborted');
@@ -99,20 +95,24 @@ export const pullApiFileByUrl = async (
9995
: url.endsWith('.java')
10096
? 'java'
10197
: 'unsupported';
102-
const sourceHash = text.startsWith('404:')
98+
const sourceFileNotFound = text.startsWith('404:');
99+
const sourceHash = sourceFileNotFound
103100
? 'not-found'
104101
: await sha256String(text);
105102
const sourceKey = `${parserKind}:${sourceHash}`;
106103
parsedSourceKey = sourceKey;
107104
const memoryCached = parsedApiFilesBySource.get(sourceKey);
108105
if (memoryCached) {
109-
return rememberParsedApiFile(sourceKey, memoryCached);
106+
return {
107+
file: rememberParsedApiFile(sourceKey, memoryCached),
108+
sourceFileNotFound,
109+
};
110110
}
111-
const parsed = await getOrSetApiFileCache(
111+
const parsedEntry = await getOrSetApiFileCache(
112112
`struct-content:${sourceKey}`,
113-
() =>
114-
getOrParseApiFile(sourceKey, async () => {
115-
if (text.startsWith('404:') || parserKind === 'unsupported') {
113+
async () => ({
114+
file: await getOrParseApiFile(sourceKey, async () => {
115+
if (sourceFileNotFound || parserKind === 'unsupported') {
116116
return {
117117
package: '',
118118
imports: [],
@@ -123,22 +123,24 @@ export const pullApiFileByUrl = async (
123123
? androidApiParser.parseAIDLFile(text)
124124
: androidApiParser.parseJavaFile(text);
125125
}),
126+
sourceFileNotFound,
127+
}),
126128
);
127-
replaceParsedApiFile(sourceKey, parsed);
128-
return parsed;
129+
replaceParsedApiFile(sourceKey, parsedEntry.file);
130+
return parsedEntry;
129131
}).catch(() => {});
130-
if (!file) {
132+
if (!entry) {
131133
return {
132134
package: '',
133135
imports: [],
134136
structs: emptyArray,
135137
};
136138
}
137-
if (file.structs.length === 0) {
138-
const is404 = await check404File(url);
139-
if (is404) {
140-
notFoundFileMap[filePath] = is404;
141-
}
139+
const { file, sourceFileNotFound } = entry;
140+
if (sourceFileNotFound) {
141+
notFoundFileMap[filePath] = true;
142+
} else {
143+
delete notFoundFileMap[filePath];
142144
}
143145
if (parsedSourceKey) {
144146
replaceParsedApiFile(parsedSourceKey, file);

web/src/utils/cache.ts

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import type { ApiFile } from '@android-cs/api-parser';
2-
import { BoundedContentValueInterner } from '@android-cs/api-query';
1+
import {
2+
BoundedContentValueInterner,
3+
type AndroidApiStructCacheEntry,
4+
} from '@android-cs/api-query';
5+
import {
6+
decodeApiFileCacheEntry,
7+
encodeApiFileCacheEntry,
8+
} from './cache/apiFileEntry';
39
import {
410
readCacheValue,
511
writeCacheBytesIfAvailable,
@@ -15,7 +21,6 @@ import { resetCacheDatabases } from './cache/database';
1521
import {
1622
broadcastCacheReset,
1723
getCacheEpoch,
18-
getLogicalFlight,
1924
invalidateLocalFlights,
2025
runSingleFlight,
2126
setExternalResetHandler,
@@ -26,7 +31,8 @@ interface UrlCacheKeyBuilder {
2631
(url: string): string;
2732
}
2833

29-
const apiFileInterner = new BoundedContentValueInterner<ApiFile>(256);
34+
const apiFileInterner =
35+
new BoundedContentValueInterner<AndroidApiStructCacheEntry>(256);
3036

3137
const scheduleStorageEstimateUpdate = (): void => {
3238
void updateStorageEstimate().catch(() => undefined);
@@ -79,36 +85,21 @@ export const persistentFetch = async (
7985

8086
export const getOrSetApiFileCache = async (
8187
filePath: string,
82-
fallback: () => Promise<ApiFile>,
83-
): Promise<ApiFile> => {
88+
fallback: () => Promise<AndroidApiStructCacheEntry>,
89+
): Promise<AndroidApiStructCacheEntry> => {
8490
const expectedEpoch = getCacheEpoch();
8591
const keyHash = await sha256String(filePath);
8692
return runSingleFlight(STRUCT_DOMAIN, keyHash, expectedEpoch, async () => {
8793
const cached = await readCacheValue(
8894
STRUCT_DOMAIN,
8995
keyHash,
90-
(bytes): ApiFile => {
91-
const value: unknown = JSON.parse(decodeText(bytes));
92-
if (
93-
typeof value !== 'object' ||
94-
value === null ||
95-
!('package' in value) ||
96-
!('imports' in value) ||
97-
!('structs' in value) ||
98-
typeof value.package !== 'string' ||
99-
!Array.isArray(value.imports) ||
100-
!Array.isArray(value.structs)
101-
) {
102-
throw new Error('Cached API file value is invalid');
103-
}
104-
return value as ApiFile;
105-
},
96+
decodeApiFileCacheEntry,
10697
apiFileInterner,
10798
).catch(() => undefined);
10899
if (cached !== undefined) return cached;
109100

110101
const value = await fallback();
111-
const bytes = encodeText(JSON.stringify(value));
102+
const bytes = encodeApiFileCacheEntry(value);
112103
const contentHash = await writeCacheBytesIfAvailable(
113104
STRUCT_DOMAIN,
114105
keyHash,
@@ -120,24 +111,6 @@ export const getOrSetApiFileCache = async (
120111
});
121112
};
122113

123-
export const check404File = async (filePath: string): Promise<boolean> => {
124-
const expectedEpoch = getCacheEpoch();
125-
const keyHash = await sha256String(filePath);
126-
const current = getLogicalFlight(TEXT_DOMAIN, keyHash, expectedEpoch);
127-
if (current) {
128-
try {
129-
const value: unknown = await current;
130-
return typeof value === 'string' && value.startsWith('404:');
131-
} catch {
132-
return false;
133-
}
134-
}
135-
const value = await readCacheValue(TEXT_DOMAIN, keyHash, decodeText).catch(
136-
() => undefined,
137-
);
138-
return value?.startsWith('404:') ?? false;
139-
};
140-
141114
export const clearLocalCache = async (): Promise<void> => {
142115
invalidateLocalFlights();
143116
apiFileInterner.clear();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { ApiFile } from '@android-cs/api-parser';
2+
import type { AndroidApiStructCacheEntry } from '@android-cs/api-query';
3+
4+
const encoder = new TextEncoder();
5+
const decoder = new TextDecoder('utf-8', { fatal: true });
6+
7+
const decodeApiFile = (value: unknown): ApiFile => {
8+
if (
9+
typeof value !== 'object' ||
10+
value === null ||
11+
!('package' in value) ||
12+
!('imports' in value) ||
13+
!('structs' in value) ||
14+
typeof value.package !== 'string' ||
15+
!Array.isArray(value.imports) ||
16+
!Array.isArray(value.structs)
17+
) {
18+
throw new Error('Cached API file entry is invalid');
19+
}
20+
return value as ApiFile;
21+
};
22+
23+
export const encodeApiFileCacheEntry = (
24+
entry: AndroidApiStructCacheEntry,
25+
): Uint8Array => encoder.encode(JSON.stringify(entry));
26+
27+
export const decodeApiFileCacheEntry = (
28+
bytes: Uint8Array,
29+
): AndroidApiStructCacheEntry => {
30+
const value: unknown = JSON.parse(decoder.decode(bytes));
31+
if (
32+
typeof value !== 'object' ||
33+
value === null ||
34+
!('file' in value) ||
35+
!('sourceFileNotFound' in value) ||
36+
typeof value.sourceFileNotFound !== 'boolean'
37+
) {
38+
throw new Error('Cached API file entry is invalid');
39+
}
40+
return {
41+
file: decodeApiFile(value.file),
42+
sourceFileNotFound: value.sourceFileNotFound,
43+
};
44+
};

web/src/utils/cache/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const CACHE_VERSIONS = {
33
format: 1,
44
codec: 'gzip',
55
text: 1,
6-
struct: 13,
6+
struct: 14,
77
} as const;
88
const CACHE_VERSION_KEY =
99
`${CACHE_VERSIONS.format}:${CACHE_VERSIONS.codec}:${CACHE_VERSIONS.text}:${CACHE_VERSIONS.struct}` as const;
@@ -15,6 +15,7 @@ const DATABASE_VERSION_BY_CACHE_VERSION = {
1515
'1:gzip:1:11': 1,
1616
'1:gzip:1:12': 2,
1717
'1:gzip:1:13': 3,
18+
'1:gzip:1:14': 4,
1819
} as const;
1920
export const DATABASE_VERSION =
2021
DATABASE_VERSION_BY_CACHE_VERSION[CACHE_VERSION_KEY];

0 commit comments

Comments
 (0)