2025-12-07 19:32:53 +02:00
|
|
|
import React, { type JSX, type ReactNode } from 'react';
|
|
|
|
|
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
|
2026-03-31 18:47:00 +03:00
|
|
|
import type { FilesAPI, RuntimeAPIs } from '@/lib/api/types';
|
|
|
|
|
import {
|
|
|
|
|
approxStringBytes,
|
|
|
|
|
evictContentLru,
|
|
|
|
|
setContentBytes,
|
|
|
|
|
touchContent as touchContentLru,
|
|
|
|
|
removeContentBytes,
|
|
|
|
|
} from '@/sync/content-cache';
|
|
|
|
|
|
|
|
|
|
/** Wrap a FilesAPI with an in-memory LRU content cache. */
|
|
|
|
|
function withContentCache(files: FilesAPI): FilesAPI {
|
|
|
|
|
const cache = new Map<string, { content: string; path: string }>();
|
|
|
|
|
|
|
|
|
|
const cachedReadFile: FilesAPI['readFile'] = files.readFile
|
|
|
|
|
? async (path: string) => {
|
|
|
|
|
const hit = cache.get(path);
|
|
|
|
|
if (hit) {
|
|
|
|
|
touchContentLru(path);
|
|
|
|
|
return hit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await files.readFile!(path);
|
|
|
|
|
const bytes = approxStringBytes(result.content);
|
|
|
|
|
cache.set(path, result);
|
|
|
|
|
setContentBytes(path, bytes);
|
|
|
|
|
|
|
|
|
|
// Evict if over limits
|
|
|
|
|
const keep = new Set<string>();
|
|
|
|
|
evictContentLru(keep, (evictPath) => {
|
|
|
|
|
cache.delete(evictPath);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
// Invalidate cache on writes, deletes, renames
|
|
|
|
|
const cachedWriteFile: FilesAPI['writeFile'] = files.writeFile
|
|
|
|
|
? async (path, content) => {
|
|
|
|
|
cache.delete(path);
|
|
|
|
|
removeContentBytes(path);
|
|
|
|
|
return files.writeFile!(path, content);
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const cachedDelete: FilesAPI['delete'] = files.delete
|
|
|
|
|
? async (path) => {
|
|
|
|
|
cache.delete(path);
|
|
|
|
|
removeContentBytes(path);
|
|
|
|
|
return files.delete!(path);
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
const cachedRename: FilesAPI['rename'] = files.rename
|
|
|
|
|
? async (oldPath, newPath) => {
|
|
|
|
|
cache.delete(oldPath);
|
|
|
|
|
removeContentBytes(oldPath);
|
|
|
|
|
cache.delete(newPath);
|
|
|
|
|
removeContentBytes(newPath);
|
|
|
|
|
return files.rename!(oldPath, newPath);
|
|
|
|
|
}
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...files,
|
|
|
|
|
readFile: cachedReadFile,
|
|
|
|
|
writeFile: cachedWriteFile,
|
|
|
|
|
delete: cachedDelete,
|
|
|
|
|
rename: cachedRename,
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-12-07 19:32:53 +02:00
|
|
|
|
|
|
|
|
export function RuntimeAPIProvider({ apis, children }: { apis: RuntimeAPIs; children: ReactNode }): JSX.Element {
|
2026-03-31 18:47:00 +03:00
|
|
|
const cachedApis = React.useMemo<RuntimeAPIs>(
|
|
|
|
|
() => ({
|
|
|
|
|
...apis,
|
|
|
|
|
files: withContentCache(apis.files),
|
|
|
|
|
}),
|
|
|
|
|
[apis],
|
|
|
|
|
);
|
|
|
|
|
return <RuntimeAPIContext.Provider value={cachedApis}>{children}</RuntimeAPIContext.Provider>;
|
2025-12-07 19:32:53 +02:00
|
|
|
}
|