veryfront 0.1.86 → 0.1.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/deno.js +1 -1
- package/esm/src/cache/distributed-cache-init.d.ts +9 -0
- package/esm/src/cache/distributed-cache-init.d.ts.map +1 -1
- package/esm/src/cache/distributed-cache-init.js +70 -52
- package/esm/src/cache/module-cache.d.ts +6 -2
- package/esm/src/cache/module-cache.d.ts.map +1 -1
- package/esm/src/cache/module-cache.js +73 -56
- package/esm/src/platform/adapters/fs/veryfront/content-metrics.d.ts +1 -1
- package/esm/src/platform/adapters/fs/veryfront/content-metrics.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/content-metrics.js +7 -1
- package/esm/src/platform/adapters/fs/veryfront/file-list-index.d.ts +10 -0
- package/esm/src/platform/adapters/fs/veryfront/file-list-index.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/file-list-index.js +83 -19
- package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.d.ts +5 -0
- package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/read-operations-helpers.js +6 -0
- package/esm/src/platform/adapters/fs/veryfront/read-operations.d.ts.map +1 -1
- package/esm/src/platform/adapters/fs/veryfront/read-operations.js +53 -34
- package/esm/src/rendering/page-resolution/page-resolver.d.ts.map +1 -1
- package/esm/src/rendering/page-resolution/page-resolver.js +10 -4
- package/esm/src/transforms/esm/http-cache-wrapper.d.ts +3 -0
- package/esm/src/transforms/esm/http-cache-wrapper.d.ts.map +1 -1
- package/esm/src/transforms/esm/http-cache-wrapper.js +21 -7
- package/esm/src/utils/version.d.ts +1 -1
- package/esm/src/utils/version.js +1 -1
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/cache/distributed-cache-init.ts +95 -59
- package/src/src/cache/module-cache.ts +95 -57
- package/src/src/platform/adapters/fs/veryfront/content-metrics.ts +13 -2
- package/src/src/platform/adapters/fs/veryfront/file-list-index.ts +101 -18
- package/src/src/platform/adapters/fs/veryfront/read-operations-helpers.ts +10 -0
- package/src/src/platform/adapters/fs/veryfront/read-operations.ts +87 -30
- package/src/src/rendering/page-resolution/page-resolver.ts +17 -6
- package/src/src/transforms/esm/http-cache-wrapper.ts +28 -7
- package/src/src/utils/version.ts +1 -1
|
@@ -17,6 +17,11 @@ const logger = rendererLogger.component("module-cache");
|
|
|
17
17
|
let moduleCache: LRUCache<string, string> | null = null;
|
|
18
18
|
let esmCache: LRUCache<string, string> | null = null;
|
|
19
19
|
|
|
20
|
+
export interface ModuleCacheMap extends Map<string, string> {
|
|
21
|
+
getOrInsert(key: string, value: string): string;
|
|
22
|
+
getOrInsertComputed(key: string, callback: (key: string) => string): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
interface ModuleCacheStats {
|
|
21
26
|
moduleCache: {
|
|
22
27
|
size: number;
|
|
@@ -89,71 +94,104 @@ export function getEsmCache(): LRUCache<string, string> {
|
|
|
89
94
|
return getOrInitPodCache(esmPodCacheOptions);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
export function createModuleCache():
|
|
97
|
+
export function createModuleCache(): ModuleCacheMap {
|
|
93
98
|
return createMapInterface(getModuleCache());
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
export function createEsmCache():
|
|
101
|
+
export function createEsmCache(): ModuleCacheMap {
|
|
97
102
|
return createMapInterface(getEsmCache());
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
function createMapInterface(cache: LRUCache<string, string>):
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
105
|
+
function createMapInterface(cache: LRUCache<string, string>): ModuleCacheMap {
|
|
106
|
+
return new LRUBackedMap(cache);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class LRUBackedMap implements ModuleCacheMap {
|
|
110
|
+
readonly [Symbol.toStringTag] = "Map";
|
|
111
|
+
|
|
112
|
+
constructor(private readonly cache: LRUCache<string, string>) {
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get(key: string): string | undefined {
|
|
116
|
+
return this.cache.get(key);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
set(key: string, value: string): this {
|
|
120
|
+
this.cache.set(key, value);
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
has(key: string): boolean {
|
|
125
|
+
return this.cache.has(key);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
delete(key: string): boolean {
|
|
129
|
+
return this.cache.delete(key);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
clear(): void {
|
|
133
|
+
this.cache.clear();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getOrInsert(key: string, value: string): string {
|
|
137
|
+
const existing = this.cache.get(key);
|
|
138
|
+
if (existing !== undefined) return existing;
|
|
139
|
+
|
|
140
|
+
this.cache.set(key, value);
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getOrInsertComputed(key: string, callback: (key: string) => string): string {
|
|
145
|
+
const existing = this.cache.get(key);
|
|
146
|
+
if (existing !== undefined) return existing;
|
|
147
|
+
|
|
148
|
+
const value = callback(key);
|
|
149
|
+
this.cache.set(key, value);
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
get size(): number {
|
|
154
|
+
return this.cache.size;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
keys(): MapIterator<string> {
|
|
158
|
+
return this.cache.keys() as unknown as MapIterator<string>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
values(): MapIterator<string> {
|
|
162
|
+
const keysIter = this.cache.keys();
|
|
163
|
+
const cacheRef = this.cache;
|
|
164
|
+
return (function* () {
|
|
165
|
+
for (const key of keysIter) {
|
|
166
|
+
const value = cacheRef.get(key);
|
|
167
|
+
if (value !== undefined) yield value;
|
|
148
168
|
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return map.entries();
|
|
152
|
-
},
|
|
153
|
-
[Symbol.toStringTag]: "Map",
|
|
154
|
-
};
|
|
169
|
+
})() as unknown as MapIterator<string>;
|
|
170
|
+
}
|
|
155
171
|
|
|
156
|
-
|
|
172
|
+
entries(): MapIterator<[string, string]> {
|
|
173
|
+
const keysIter = this.cache.keys();
|
|
174
|
+
const cacheRef = this.cache;
|
|
175
|
+
return (function* () {
|
|
176
|
+
for (const key of keysIter) {
|
|
177
|
+
const value = cacheRef.get(key);
|
|
178
|
+
if (value !== undefined) yield [key, value] as [string, string];
|
|
179
|
+
}
|
|
180
|
+
})() as unknown as MapIterator<[string, string]>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
forEach(
|
|
184
|
+
callback: (value: string, key: string, map: Map<string, string>) => void,
|
|
185
|
+
thisArg?: unknown,
|
|
186
|
+
): void {
|
|
187
|
+
for (const [key, value] of this.entries()) {
|
|
188
|
+
callback.call(thisArg, value, key, this);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
[Symbol.iterator](): MapIterator<[string, string]> {
|
|
193
|
+
return this.entries();
|
|
194
|
+
}
|
|
157
195
|
}
|
|
158
196
|
|
|
159
197
|
export function getModuleCacheStats(): ModuleCacheStats {
|
|
@@ -8,7 +8,12 @@ import {
|
|
|
8
8
|
const logger = baseLogger.component("content-metrics");
|
|
9
9
|
|
|
10
10
|
type FileType = "page" | "layout" | "component" | "api" | "data" | "config" | "other";
|
|
11
|
-
export type MissReason =
|
|
11
|
+
export type MissReason =
|
|
12
|
+
| "cold_start"
|
|
13
|
+
| "not_in_filelist"
|
|
14
|
+
| "invalidation"
|
|
15
|
+
| "no_filelist_cache"
|
|
16
|
+
| "indexed_without_content";
|
|
12
17
|
|
|
13
18
|
interface PerRequestMetrics {
|
|
14
19
|
startTime: number;
|
|
@@ -52,7 +57,13 @@ function createFreshRequestMetrics(): PerRequestMetrics {
|
|
|
52
57
|
networkFetches: 0,
|
|
53
58
|
networkMs: 0,
|
|
54
59
|
fetchesByType: { page: 0, layout: 0, component: 0, api: 0, data: 0, config: 0, other: 0 },
|
|
55
|
-
missReasons: {
|
|
60
|
+
missReasons: {
|
|
61
|
+
cold_start: 0,
|
|
62
|
+
not_in_filelist: 0,
|
|
63
|
+
invalidation: 0,
|
|
64
|
+
no_filelist_cache: 0,
|
|
65
|
+
indexed_without_content: 0,
|
|
66
|
+
},
|
|
56
67
|
isPreviewMode: null,
|
|
57
68
|
filesAccessed: new Set(),
|
|
58
69
|
};
|
|
@@ -7,6 +7,13 @@ interface FileListCacheEntry {
|
|
|
7
7
|
content?: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export interface FileListMatchResult {
|
|
11
|
+
status: "unavailable" | "missing" | "present_without_content" | "hit";
|
|
12
|
+
fresh: boolean;
|
|
13
|
+
path?: string;
|
|
14
|
+
content?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
10
17
|
function hashPreview(content: string): number {
|
|
11
18
|
return content
|
|
12
19
|
.slice(0, 100)
|
|
@@ -22,8 +29,10 @@ const INDEX_STALENESS_LIMIT_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
22
29
|
|
|
23
30
|
export class FileListIndex {
|
|
24
31
|
private index: Map<string, string> | null = null;
|
|
32
|
+
private pathSet: Set<string> | null = null;
|
|
25
33
|
private indexKey: string | null = null;
|
|
26
34
|
private indexBuiltAt = 0;
|
|
35
|
+
private indexFresh = false;
|
|
27
36
|
private readyPromise: Promise<void> | null = null;
|
|
28
37
|
|
|
29
38
|
constructor(
|
|
@@ -39,8 +48,10 @@ export class FileListIndex {
|
|
|
39
48
|
|
|
40
49
|
const indexedWithContent = this.index.size;
|
|
41
50
|
this.index = null;
|
|
51
|
+
this.pathSet = null;
|
|
42
52
|
this.indexKey = null;
|
|
43
53
|
this.indexBuiltAt = 0;
|
|
54
|
+
this.indexFresh = false;
|
|
44
55
|
logger.debug("Cleared file list index", { indexedWithContent });
|
|
45
56
|
}
|
|
46
57
|
|
|
@@ -56,21 +67,39 @@ export class FileListIndex {
|
|
|
56
67
|
}
|
|
57
68
|
|
|
58
69
|
async lookup(normalizedPath: string): Promise<string | undefined> {
|
|
70
|
+
const match = await this.match(normalizedPath);
|
|
71
|
+
return match.status === "hit" ? match.content : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async match(normalizedPath: string): Promise<FileListMatchResult> {
|
|
59
75
|
await this.ensureReady();
|
|
60
76
|
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
77
|
+
const snapshot = await this.getOrBuild();
|
|
78
|
+
if (!snapshot) {
|
|
63
79
|
logger.debug("No file list cache available");
|
|
64
|
-
return
|
|
80
|
+
return { status: "unavailable", fresh: false };
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
if (!content) {
|
|
83
|
+
if (!snapshot.paths.has(normalizedPath)) {
|
|
69
84
|
logger.debug("Content not in file list index", {
|
|
70
85
|
path: normalizedPath,
|
|
71
|
-
indexSize:
|
|
86
|
+
indexSize: snapshot.content.size,
|
|
87
|
+
fresh: snapshot.fresh,
|
|
72
88
|
});
|
|
73
|
-
return
|
|
89
|
+
return { status: "missing", fresh: snapshot.fresh };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const content = snapshot.content.get(normalizedPath);
|
|
93
|
+
if (!content) {
|
|
94
|
+
logger.debug("File list index contains path without inline content", {
|
|
95
|
+
path: normalizedPath,
|
|
96
|
+
fresh: snapshot.fresh,
|
|
97
|
+
});
|
|
98
|
+
return {
|
|
99
|
+
status: "present_without_content",
|
|
100
|
+
fresh: snapshot.fresh,
|
|
101
|
+
path: normalizedPath,
|
|
102
|
+
};
|
|
74
103
|
}
|
|
75
104
|
|
|
76
105
|
logger.debug("FILE_LIST_CACHE_HIT - serving from file list cache", {
|
|
@@ -80,26 +109,60 @@ export class FileListIndex {
|
|
|
80
109
|
contentPreview: previewText(content, 200).replace(/\n/g, "\\n"),
|
|
81
110
|
});
|
|
82
111
|
|
|
83
|
-
return
|
|
112
|
+
return {
|
|
113
|
+
status: "hit",
|
|
114
|
+
fresh: snapshot.fresh,
|
|
115
|
+
path: normalizedPath,
|
|
116
|
+
content,
|
|
117
|
+
};
|
|
84
118
|
}
|
|
85
119
|
|
|
86
120
|
async findFirstWithContent(
|
|
87
121
|
normalizedPaths: string[],
|
|
88
122
|
): Promise<{ path: string; content: string } | undefined> {
|
|
123
|
+
const match = await this.findFirstMatch(normalizedPaths);
|
|
124
|
+
if (match.status !== "hit" || !match.path || !match.content) return undefined;
|
|
125
|
+
return { path: match.path, content: match.content };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async findFirstMatch(
|
|
129
|
+
normalizedPaths: string[],
|
|
130
|
+
): Promise<FileListMatchResult> {
|
|
89
131
|
await this.ensureReady();
|
|
90
132
|
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
133
|
+
const snapshot = await this.getOrBuild();
|
|
134
|
+
if (!snapshot) return { status: "unavailable", fresh: false };
|
|
93
135
|
|
|
94
136
|
for (const path of normalizedPaths) {
|
|
95
|
-
|
|
96
|
-
|
|
137
|
+
if (!snapshot.paths.has(path)) continue;
|
|
138
|
+
|
|
139
|
+
const content = snapshot.content.get(path);
|
|
140
|
+
if (content) {
|
|
141
|
+
return {
|
|
142
|
+
status: "hit",
|
|
143
|
+
fresh: snapshot.fresh,
|
|
144
|
+
path,
|
|
145
|
+
content,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
status: "present_without_content",
|
|
151
|
+
fresh: snapshot.fresh,
|
|
152
|
+
path,
|
|
153
|
+
};
|
|
97
154
|
}
|
|
98
155
|
|
|
99
|
-
return
|
|
156
|
+
return { status: "missing", fresh: snapshot.fresh };
|
|
100
157
|
}
|
|
101
158
|
|
|
102
|
-
private async getOrBuild(): Promise<
|
|
159
|
+
private async getOrBuild(): Promise<
|
|
160
|
+
{
|
|
161
|
+
content: Map<string, string>;
|
|
162
|
+
paths: Set<string>;
|
|
163
|
+
fresh: boolean;
|
|
164
|
+
} | null
|
|
165
|
+
> {
|
|
103
166
|
if (!this.getFileListCache) {
|
|
104
167
|
logger.debug("getOrBuildFileListIndex: no getFileListCache function");
|
|
105
168
|
return null;
|
|
@@ -118,7 +181,12 @@ export class FileListIndex {
|
|
|
118
181
|
indexSize: this.index.size,
|
|
119
182
|
indexAgeMs: age,
|
|
120
183
|
});
|
|
121
|
-
|
|
184
|
+
this.indexFresh = false;
|
|
185
|
+
return {
|
|
186
|
+
content: this.index,
|
|
187
|
+
paths: this.pathSet ?? new Set<string>(),
|
|
188
|
+
fresh: false,
|
|
189
|
+
};
|
|
122
190
|
}
|
|
123
191
|
logger.debug("getOrBuildFileListIndex: in-memory index too stale, discarding", {
|
|
124
192
|
indexSize: this.index.size,
|
|
@@ -126,7 +194,9 @@ export class FileListIndex {
|
|
|
126
194
|
staleLimitMs: INDEX_STALENESS_LIMIT_MS,
|
|
127
195
|
});
|
|
128
196
|
this.index = null;
|
|
197
|
+
this.pathSet = null;
|
|
129
198
|
this.indexKey = null;
|
|
199
|
+
this.indexFresh = false;
|
|
130
200
|
}
|
|
131
201
|
logger.debug(
|
|
132
202
|
"[ReadOperations] getOrBuildFileListIndex: getFileListCache returned null/undefined",
|
|
@@ -146,19 +216,28 @@ export class FileListIndex {
|
|
|
146
216
|
const indexKey = `${fileList.length}:${fileList[0]?.path ?? ""}:${
|
|
147
217
|
fileList[fileList.length - 1]?.path ?? ""
|
|
148
218
|
}`;
|
|
149
|
-
if (this.index && this.indexKey === indexKey) {
|
|
219
|
+
if (this.index && this.pathSet && this.indexKey === indexKey) {
|
|
150
220
|
this.indexBuiltAt = Date.now();
|
|
151
|
-
|
|
221
|
+
this.indexFresh = true;
|
|
222
|
+
return {
|
|
223
|
+
content: this.index,
|
|
224
|
+
paths: this.pathSet,
|
|
225
|
+
fresh: true,
|
|
226
|
+
};
|
|
152
227
|
}
|
|
153
228
|
|
|
154
229
|
const index = new Map<string, string>();
|
|
230
|
+
const pathSet = new Set<string>();
|
|
155
231
|
for (const file of fileList) {
|
|
232
|
+
pathSet.add(file.path);
|
|
156
233
|
if (file.content) index.set(file.path, file.content);
|
|
157
234
|
}
|
|
158
235
|
|
|
159
236
|
this.index = index;
|
|
237
|
+
this.pathSet = pathSet;
|
|
160
238
|
this.indexKey = indexKey;
|
|
161
239
|
this.indexBuiltAt = Date.now();
|
|
240
|
+
this.indexFresh = true;
|
|
162
241
|
|
|
163
242
|
const sampleFile = fileList.find((f) => /welcome/i.test(f.path));
|
|
164
243
|
const sampleContent = sampleFile?.content;
|
|
@@ -171,6 +250,10 @@ export class FileListIndex {
|
|
|
171
250
|
sampleContentPreview: sampleContent?.slice(0, 200)?.replace(/\n/g, "\\n"),
|
|
172
251
|
});
|
|
173
252
|
|
|
174
|
-
return
|
|
253
|
+
return {
|
|
254
|
+
content: index,
|
|
255
|
+
paths: pathSet,
|
|
256
|
+
fresh: true,
|
|
257
|
+
};
|
|
175
258
|
}
|
|
176
259
|
}
|
|
@@ -6,6 +6,8 @@ import type { ResolvedContentContext } from "./types.js";
|
|
|
6
6
|
|
|
7
7
|
export { READ_OPERATION_EXTENSION_PRIORITY };
|
|
8
8
|
|
|
9
|
+
export type NotFoundLikeError = Error & { code?: string };
|
|
10
|
+
|
|
9
11
|
interface ReadContextProviderLike {
|
|
10
12
|
isProductionMode: () => boolean;
|
|
11
13
|
isPersistentCacheInvalidated?: (prefix: string) => boolean;
|
|
@@ -78,6 +80,10 @@ export function getResolvedCacheKey(
|
|
|
78
80
|
return `${cacheKeyPrefix}:${normalizedResolvedPath}`;
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
export function buildExtensionCandidatePaths(basePath: string): string[] {
|
|
84
|
+
return READ_OPERATION_EXTENSION_PRIORITY.map((ext) => `${basePath}${ext}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
export function splitKnownFileExtension(
|
|
82
88
|
apiPath: string,
|
|
83
89
|
): { originalExtension: string; basePath: string } | null {
|
|
@@ -95,3 +101,7 @@ export function isNotFoundLikeError(error: unknown): boolean {
|
|
|
95
101
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
96
102
|
return errorMessage.includes("404") || errorMessage.includes("Not Found");
|
|
97
103
|
}
|
|
104
|
+
|
|
105
|
+
export function createNotFoundLikeError(path: string): NotFoundLikeError {
|
|
106
|
+
return Object.assign(new Error(`404 Not Found: ${path}`), { code: "ENOENT" });
|
|
107
|
+
}
|
|
@@ -3,13 +3,15 @@ import { withSpan } from "../../../../observability/tracing/otlp-setup.js";
|
|
|
3
3
|
import type { VeryfrontApiClient } from "../../veryfront-api-client/index.js";
|
|
4
4
|
import { FileCache } from "../cache/file-cache.js";
|
|
5
5
|
import { logContentMetric, type MissReason } from "./content-metrics.js";
|
|
6
|
-
import { FileListIndex } from "./file-list-index.js";
|
|
6
|
+
import { FileListIndex, type FileListMatchResult } from "./file-list-index.js";
|
|
7
7
|
import { InFlightRequestDeduper } from "./in-flight-dedupe.js";
|
|
8
8
|
import { getRequestScopedFile, setRequestScopedFile } from "./multi-project-adapter.js";
|
|
9
9
|
import { PathNormalizer } from "./path-normalizer.js";
|
|
10
10
|
import {
|
|
11
11
|
assertProjectSourcePath,
|
|
12
|
+
buildExtensionCandidatePaths,
|
|
12
13
|
buildReadFetchState,
|
|
14
|
+
createNotFoundLikeError,
|
|
13
15
|
getResolvedCacheKey,
|
|
14
16
|
isNotFoundLikeError,
|
|
15
17
|
READ_OPERATION_EXTENSION_PRIORITY as EXTENSION_PRIORITY,
|
|
@@ -184,17 +186,23 @@ export class ReadOperations {
|
|
|
184
186
|
skipPersistentCaches: boolean,
|
|
185
187
|
isPreviewMode: boolean,
|
|
186
188
|
ctx: ResolvedContentContext | null,
|
|
187
|
-
): Promise<
|
|
189
|
+
): Promise<FileListMatchResult> {
|
|
188
190
|
// File list cache is enabled for BOTH preview and production modes.
|
|
189
191
|
// The file list is an in-memory index built from API response at init, updated by WebSocket pokes.
|
|
190
192
|
// This is safe because:
|
|
191
193
|
// - File list is refreshed on every WebSocket poke (websocket-manager.ts:483-500)
|
|
192
194
|
// - Request-scoped cache ensures consistency within a single render
|
|
193
195
|
// - Persistent cache is only written for production mode (to avoid staleness risk in preview)
|
|
194
|
-
if (
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
if (skipPersistentCaches) {
|
|
197
|
+
logger.debug("Skipping file list cache due to invalidation", {
|
|
198
|
+
path: normalizedPath,
|
|
199
|
+
cacheKeyPrefix,
|
|
200
|
+
});
|
|
201
|
+
return { status: "unavailable", fresh: false };
|
|
202
|
+
}
|
|
197
203
|
|
|
204
|
+
const match = await this.fileListIndex.match(normalizedPath);
|
|
205
|
+
if (match.status === "hit" && match.content) {
|
|
198
206
|
logContentMetric("FILE_LIST_HIT", {
|
|
199
207
|
path: normalizedPath,
|
|
200
208
|
mode: ctx?.sourceType ?? "unknown",
|
|
@@ -204,24 +212,12 @@ export class ReadOperations {
|
|
|
204
212
|
// Only cache to persistent storage for production mode
|
|
205
213
|
// Preview mode uses file list cache directly without persisting (fresher, WebSocket-driven)
|
|
206
214
|
if (isProduction) {
|
|
207
|
-
this.cache.set(cacheKey,
|
|
215
|
+
this.cache.set(cacheKey, match.content);
|
|
208
216
|
}
|
|
209
|
-
setRequestScopedFile(cacheKey,
|
|
210
|
-
return fileListContent;
|
|
217
|
+
setRequestScopedFile(cacheKey, match.content);
|
|
211
218
|
}
|
|
212
219
|
|
|
213
|
-
|
|
214
|
-
logContentMetric("CACHE_MISS", {
|
|
215
|
-
path: normalizedPath,
|
|
216
|
-
mode: ctx?.sourceType ?? "unknown",
|
|
217
|
-
missReason: "invalidation" as MissReason,
|
|
218
|
-
isPreviewMode,
|
|
219
|
-
});
|
|
220
|
-
logger.debug("Skipping file list cache due to invalidation", {
|
|
221
|
-
path: normalizedPath,
|
|
222
|
-
cacheKeyPrefix,
|
|
223
|
-
});
|
|
224
|
-
return null;
|
|
220
|
+
return match;
|
|
225
221
|
}
|
|
226
222
|
|
|
227
223
|
private setupInFlightFetch(
|
|
@@ -232,6 +228,7 @@ export class ReadOperations {
|
|
|
232
228
|
isProduction: boolean,
|
|
233
229
|
isPreviewMode: boolean,
|
|
234
230
|
ctx: ResolvedContentContext | null,
|
|
231
|
+
missReason: MissReason,
|
|
235
232
|
): Promise<string> {
|
|
236
233
|
const cleanupResult = this.inFlightRequests.cleanup();
|
|
237
234
|
if (cleanupResult) {
|
|
@@ -249,11 +246,10 @@ export class ReadOperations {
|
|
|
249
246
|
}
|
|
250
247
|
|
|
251
248
|
// Track why we're making a network fetch (for optimization analysis)
|
|
252
|
-
const hasFileListCache = !!this.getFileListCache;
|
|
253
249
|
logContentMetric("CACHE_MISS", {
|
|
254
250
|
path: normalizedPath,
|
|
255
251
|
mode: ctx?.sourceType ?? "unknown",
|
|
256
|
-
missReason
|
|
252
|
+
missReason,
|
|
257
253
|
isPreviewMode,
|
|
258
254
|
});
|
|
259
255
|
|
|
@@ -377,10 +373,10 @@ export class ReadOperations {
|
|
|
377
373
|
isProduction: boolean,
|
|
378
374
|
ctx: ResolvedContentContext | null,
|
|
379
375
|
isPreviewMode: boolean,
|
|
380
|
-
): Promise<
|
|
381
|
-
const candidatePaths =
|
|
382
|
-
const resolved = await this.fileListIndex.
|
|
383
|
-
if (!resolved) return
|
|
376
|
+
): Promise<FileListMatchResult> {
|
|
377
|
+
const candidatePaths = buildExtensionCandidatePaths(normalizedPath);
|
|
378
|
+
const resolved = await this.fileListIndex.findFirstMatch(candidatePaths);
|
|
379
|
+
if (resolved.status !== "hit" || !resolved.path || !resolved.content) return resolved;
|
|
384
380
|
|
|
385
381
|
const resolvedCacheKey = getResolvedCacheKey(cacheKeyPrefix, resolved.path);
|
|
386
382
|
|
|
@@ -402,7 +398,7 @@ export class ReadOperations {
|
|
|
402
398
|
|
|
403
399
|
this.cacheResolvedContent(cacheKey, resolvedCacheKey, resolved.content, isProduction);
|
|
404
400
|
|
|
405
|
-
return resolved
|
|
401
|
+
return resolved;
|
|
406
402
|
}
|
|
407
403
|
|
|
408
404
|
private cacheResolvedContent(
|
|
@@ -472,7 +468,7 @@ export class ReadOperations {
|
|
|
472
468
|
);
|
|
473
469
|
if (persistentCached) return persistentCached;
|
|
474
470
|
|
|
475
|
-
const
|
|
471
|
+
const fileListMatch = await this.getFileListCacheHit(
|
|
476
472
|
normalizedPath,
|
|
477
473
|
cacheKeyPrefix,
|
|
478
474
|
cacheKey,
|
|
@@ -481,7 +477,19 @@ export class ReadOperations {
|
|
|
481
477
|
isPreviewMode,
|
|
482
478
|
ctx,
|
|
483
479
|
);
|
|
484
|
-
if (
|
|
480
|
+
if (fileListMatch.status === "hit" && fileListMatch.content) return fileListMatch.content;
|
|
481
|
+
if (fileListMatch.status === "present_without_content") {
|
|
482
|
+
return this.setupInFlightFetch(
|
|
483
|
+
normalizedPath,
|
|
484
|
+
apiPath,
|
|
485
|
+
cacheKey,
|
|
486
|
+
isPublished,
|
|
487
|
+
isProduction,
|
|
488
|
+
isPreviewMode,
|
|
489
|
+
ctx,
|
|
490
|
+
"indexed_without_content",
|
|
491
|
+
);
|
|
492
|
+
}
|
|
485
493
|
|
|
486
494
|
if (!hasKnownExt) {
|
|
487
495
|
if (!skipPersistentCaches) {
|
|
@@ -493,7 +501,47 @@ export class ReadOperations {
|
|
|
493
501
|
ctx,
|
|
494
502
|
isPreviewMode,
|
|
495
503
|
);
|
|
496
|
-
if (resolvedFromFileList
|
|
504
|
+
if (resolvedFromFileList.status === "hit" && resolvedFromFileList.content) {
|
|
505
|
+
return resolvedFromFileList.content;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (
|
|
509
|
+
resolvedFromFileList.status === "present_without_content" &&
|
|
510
|
+
resolvedFromFileList.path
|
|
511
|
+
) {
|
|
512
|
+
const resolvedCacheKey = getResolvedCacheKey(cacheKeyPrefix, resolvedFromFileList.path);
|
|
513
|
+
const resolvedApiPath = this.getOriginalApiPath?.(resolvedFromFileList.path) ??
|
|
514
|
+
resolvedFromFileList.path;
|
|
515
|
+
const fetchedResolved = await this.setupInFlightFetch(
|
|
516
|
+
resolvedFromFileList.path,
|
|
517
|
+
resolvedApiPath,
|
|
518
|
+
resolvedCacheKey,
|
|
519
|
+
isPublished,
|
|
520
|
+
isProduction,
|
|
521
|
+
isPreviewMode,
|
|
522
|
+
ctx,
|
|
523
|
+
"indexed_without_content",
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
this.extensionResolutionCache.set(normalizedPath, resolvedFromFileList.path);
|
|
527
|
+
this.cacheResolvedContent(
|
|
528
|
+
cacheKey,
|
|
529
|
+
resolvedCacheKey,
|
|
530
|
+
fetchedResolved,
|
|
531
|
+
isProduction && !skipPersistentCaches,
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
return fetchedResolved;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (
|
|
538
|
+
fileListMatch.status === "missing" &&
|
|
539
|
+
fileListMatch.fresh &&
|
|
540
|
+
resolvedFromFileList.status === "missing" &&
|
|
541
|
+
resolvedFromFileList.fresh
|
|
542
|
+
) {
|
|
543
|
+
throw createNotFoundLikeError(normalizedPath);
|
|
544
|
+
}
|
|
497
545
|
}
|
|
498
546
|
|
|
499
547
|
const resolved = await this.tryResolveExtensionlessPath(
|
|
@@ -506,6 +554,10 @@ export class ReadOperations {
|
|
|
506
554
|
if (resolved) return resolved;
|
|
507
555
|
}
|
|
508
556
|
|
|
557
|
+
if (fileListMatch.status === "missing" && fileListMatch.fresh) {
|
|
558
|
+
throw createNotFoundLikeError(normalizedPath);
|
|
559
|
+
}
|
|
560
|
+
|
|
509
561
|
return this.setupInFlightFetch(
|
|
510
562
|
normalizedPath,
|
|
511
563
|
apiPath,
|
|
@@ -514,6 +566,11 @@ export class ReadOperations {
|
|
|
514
566
|
isProduction,
|
|
515
567
|
isPreviewMode,
|
|
516
568
|
ctx,
|
|
569
|
+
skipPersistentCaches
|
|
570
|
+
? "invalidation"
|
|
571
|
+
: fileListMatch.status === "missing" && fileListMatch.fresh
|
|
572
|
+
? "not_in_filelist"
|
|
573
|
+
: "no_filelist_cache",
|
|
517
574
|
);
|
|
518
575
|
}
|
|
519
576
|
|