Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Improve env names and default
Browse files Browse the repository at this point in the history
matheusgr committed Sep 30, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent 683fadf commit 6183645
Showing 3 changed files with 242 additions and 7 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -174,6 +174,7 @@ Here is a table with the integrations that we have built and the statuses of the
| `ENABLE_LOADER_CACHE` | Flag to enable or disable the loader cache | `true` |
| `LOADER_CACHE_START_TRESHOLD` | Cache start threshold | `0` |
| `WEB_CACHE_ENGINE` | Defines the cache engine(s) to use | `"FILE_SYSTEM,CACHE_API"` |
| `FILE_SYSTEM_CACHE_DIRECTORY` | Directory path for file system cache | `/tmp` |
| `CACHE_MAX_SIZE` | Maximum size of the file system cache (in bytes) | `1073741824` (1 GB) |
| `CACHE_TTL_AUTOPURGE` | Flag to automatically delete expired items from the file system cache (cpu intensive) | `false` |
| `CACHE_TTL_RESOLUTION` | Time interval to check for expired items in the file system cache (in milliseconds) | `30000` (30 seconds) |
244 changes: 238 additions & 6 deletions runtime/caches/fileSystem.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,243 @@
const FILE_SYSTEM_CACHE_DIRECTORY = "/tmp";
import { existsSync } from "@std/fs";
import { logger } from "../../observability/otel/config.ts";

const hasWritePerm = async (fsDir: string): Promise<boolean> => {
import {
assertCanBeCached,
assertNoOptions,
withCacheNamespace
} from "./utils.ts";

const FILE_SYSTEM_CACHE_DIRECTORY =
Deno.env.get("FILE_SYSTEM_CACHE_DIRECTORY") ?? '/tmp/deco_cache';

// Function to convert headers object to a Uint8Array
function headersToUint8Array(headers: [string, string][]) {
const headersStr = JSON.stringify(headers);
return new TextEncoder().encode(headersStr);
}

// Function to combine the body and headers into a single buffer
function generateCombinedBuffer(body: Uint8Array, headers: Uint8Array) {
// This prepends the header length to the combined buffer. As it has 4 bytes in size,
// it can store up to 2^32 - 1 bytes of headers (4GB). This should be enough for all deco use cases.
const headerLength = new Uint8Array(new Uint32Array([headers.length]).buffer);

// Concatenate length, headers, and body into one Uint8Array
const combinedBuffer = new Uint8Array(
headerLength.length + headers.length + body.length,
);
combinedBuffer.set(headerLength, 0);
combinedBuffer.set(headers, headerLength.length);
combinedBuffer.set(body, headerLength.length + headers.length);
return combinedBuffer;
}

// Function to extract the headers and body from a combined buffer
function extractCombinedBuffer(combinedBuffer: Uint8Array) {
// Extract the header length from the combined buffer
const headerLengthArray = combinedBuffer.slice(0, 4);
const headerLength = new Uint32Array(headerLengthArray.buffer)[0];

// Extract the headers and body from the combined buffer
const headers = combinedBuffer.slice(4, 4 + headerLength);
const body = combinedBuffer.slice(4 + headerLength);
return { headers, body };
}

function getIterableHeaders(headers: Uint8Array) {
const headersStr = new TextDecoder().decode(headers);

// Directly parse the string as an array of [key, value] pairs
const headerPairs: [string, string][] = JSON.parse(headersStr);

// Filter out any pairs with empty key or value
const filteredHeaders = headerPairs.filter(([key, value]) =>
key !== "" && value !== ""
);
return filteredHeaders;
}

function createFileSystemCache(): CacheStorage {
let isCacheInitialized = false;
async function assertCacheDirectory() {
try {
if (
FILE_SYSTEM_CACHE_DIRECTORY && !existsSync(FILE_SYSTEM_CACHE_DIRECTORY)
) {
await Deno.mkdirSync(FILE_SYSTEM_CACHE_DIRECTORY, { recursive: true });
}
isCacheInitialized = true;
} catch (err) {
console.error("Unable to initialize file system cache directory", err);
}
}

async function putFile(
key: string,
responseArray: Uint8Array,
) {
if (!isCacheInitialized) {
await assertCacheDirectory();
}
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;

await Deno.writeFile(filePath, responseArray);
return;
}

async function getFile(key: string) {
if (!isCacheInitialized) {
await assertCacheDirectory();
}
try {
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;
const fileContent = await Deno.readFile(filePath);
return fileContent;
} catch (err) {
// Error code different for file/dir not found
// The file won't be found in cases where it's not cached
if (err.code !== "ENOENT") {
logger.error(`error when reading from file system, ${err}`);
}
return null;
}
}

async function deleteFile(key: string) {
if (!isCacheInitialized) {
await assertCacheDirectory();
}
try {
const filePath = `${FILE_SYSTEM_CACHE_DIRECTORY}/${key}`;
await Deno.remove(filePath);
return true;
} catch (err) {
logger.error(`error when deleting from file system, ${err}`);
return false;
}
}

const caches: CacheStorage = {
delete: (_cacheName: string): Promise<boolean> => {
throw new Error("Not Implemented");
},
has: (_cacheName: string): Promise<boolean> => {
throw new Error("Not Implemented");
},
keys: (): Promise<string[]> => {
throw new Error("Not Implemented");
},
match: (
_request: URL | RequestInfo,
_options?: MultiCacheQueryOptions | undefined,
): Promise<Response | undefined> => {
throw new Error("Not Implemented");
},
open: (cacheName: string): Promise<Cache> => {
const requestURLSHA1 = withCacheNamespace(cacheName);
return Promise.resolve({
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/add) */
add: (_request: RequestInfo | URL): Promise<void> => {
throw new Error("Not Implemented");
},
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/addAll) */
addAll: (_requests: RequestInfo[]): Promise<void> => {
throw new Error("Not Implemented");
},
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/delete) */
delete: async (
request: RequestInfo | URL,
options?: CacheQueryOptions,
): Promise<boolean> => {
assertNoOptions(options);

const deleteResponse = await deleteFile(
await requestURLSHA1(request),
);
return deleteResponse;
},
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/keys) */
keys: (
_request?: RequestInfo | URL,
_options?: CacheQueryOptions,
): Promise<ReadonlyArray<Request>> => {
throw new Error("Not Implemented");
},
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/match) */
match: async (
request: RequestInfo | URL,
options?: CacheQueryOptions,
): Promise<Response | undefined> => {
assertNoOptions(options);
const cacheKey = await requestURLSHA1(request);
const data = await getFile(cacheKey);

if (data === null) {
return undefined;
}

const { headers, body } = extractCombinedBuffer(data);
const iterableHeaders = getIterableHeaders(headers);
const responseHeaders = new Headers(iterableHeaders);
return new Response(
body,
{ headers: responseHeaders },
);
},
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/matchAll) */
matchAll: (
_request?: RequestInfo | URL,
_options?: CacheQueryOptions,
): Promise<ReadonlyArray<Response>> => {
throw new Error("Not Implemented");
},
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Cache/put) */
put: async (
request: RequestInfo | URL,
response: Response,
): Promise<void> => {
const req = new Request(request);
assertCanBeCached(req, response);

if (!response.body) {
return;
}

const cacheKey = await requestURLSHA1(request);

const bodyBuffer = await response.arrayBuffer()
.then((buffer) => new Uint8Array(buffer))
.then((buffer) => {
return buffer;
});
const headersBuffer = headersToUint8Array([
...response.headers.entries(),
]);
const buffer = generateCombinedBuffer(bodyBuffer, headersBuffer);

await putFile(
cacheKey,
buffer
).catch(
(err) => {
console.error("file system error", err);
},
);
},
});
},
};

return caches;
}

const hasWritePerm = async (): Promise<boolean> => {
return await Deno.permissions.query(
{ name: "write", path: fsDir } as const,
{ name: "write", path: FILE_SYSTEM_CACHE_DIRECTORY } as const,
).then((status) => status.state === "granted");
};

export const isFileSystemAvailable =
FILE_SYSTEM_CACHE_DIRECTORY !== undefined &&
await hasWritePerm(FILE_SYSTEM_CACHE_DIRECTORY);
export const isFileSystemAvailable = await hasWritePerm() &&
FILE_SYSTEM_CACHE_DIRECTORY !== undefined;

export const caches = createFileSystemCache();
4 changes: 3 additions & 1 deletion runtime/caches/mod.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@ import { createTieredCache } from "./tiered.ts";

import { caches as lruCache } from "./lrucache.ts";

import { caches as fileSystem } from "./fileSystem.ts";

export const ENABLE_LOADER_CACHE: boolean =
Deno.env.get("ENABLE_LOADER_CACHE") !== "false";
const DEFAULT_CACHE_ENGINE = "CACHE_API";
@@ -30,7 +32,7 @@ export const cacheImplByEngine: Record<CacheEngine, CacheStorageOption> = {
isAvailable: typeof globalThis.caches !== "undefined",
},
FILE_SYSTEM: {
implementation: headersCache(lruCache(globalThis.caches)),
implementation: headersCache(lruCache(fileSystem)),
isAvailable: isFileSystemAvailable,
},
};

0 comments on commit 6183645

Please sign in to comment.