diff --git a/components/HttpClient/CacheEntry.php b/components/HttpClient/CacheEntry.php deleted file mode 100644 index 74a5759..0000000 --- a/components/HttpClient/CacheEntry.php +++ /dev/null @@ -1,34 +0,0 @@ -true, 'max-age'=>60, …] */ - public static function directives( ?string $value ): array { - if ( $value === null ) { - return []; - } - $out = []; - foreach ( explode( ',', $value ) as $part ) { - $part = trim( $part ); - if ( $part === '' ) { - continue; - } - if ( strpos( $part, '=' ) !== false ) { - [ $k, $v ] = array_map( 'trim', explode( '=', $part, 2 ) ); - $out[ strtolower( $k ) ] = ctype_digit( $v ) ? (int) $v : strtolower( $v ); - } else { - $out[ strtolower( $part ) ] = true; - } - } - - return $out; - } - - public static function response_is_cacheable( Response $r ): bool { - $req = $r->request; - if ( $req->method !== 'GET' ) { - return false; - } - if ( $r->status_code !== 200 && $r->status_code !== 206 ) { - return false; - } - - $d = self::directives( $r->get_header( 'cache-control' ) ); - if ( isset( $d['no-store'] ) ) { - return false; - } - if ( $r->get_header( 'expires' ) || isset( $d['max-age'] ) || isset( $d['s-maxage'] ) ) { - return true; - } - - // heuristic: if Last-Modified present and older than 24 h cache for 10 % - return (bool) $r->get_header( 'last-modified' ); - } - - public static function freshness_lifetime( CacheEntry $e ): int { - $h = $e->headers; - $d = self::directives( $h['cache-control'] ?? null ); - if ( isset( $d['s-maxage'] ) ) { - return $d['s-maxage']; - } - if ( isset( $d['max-age'] ) ) { - return $d['max-age']; - } - if ( isset( $h['expires'] ) ) { - return max( 0, strtotime( $h['expires'] ) - $e->stored_at ); - } - - if ( isset( $h['last-modified'] ) ) { - $age = $e->stored_at - strtotime( $h['last-modified'] ); - - return (int) max( 0, $age / 10 ); - } - - return 0; // treat as immediately stale - } - - public static function is_fresh( CacheEntry $e, ?int $now = null ): bool { - $now = $now ?? time(); - $age_hdr = (int) ( $e->headers['age'] ?? 0 ); - $current_age = $age_hdr + ( $now - $e->stored_at ); - - return $current_age < self::freshness_lifetime( $e ); - } - - public static function must_revalidate( CacheEntry $e ): bool { - $d = self::directives( $e->headers['cache-control'] ?? null ); - - return isset( $d['must-revalidate'] ) || isset( $d['proxy-revalidate'] ); - } - - public static function parse_max_age( string $cc ): ?int { - foreach ( explode( ',', $cc ) as $d ) { - $d = trim( $d ); - if ( strncmp( $d, 'max-age=', strlen( 'max-age=' ) ) === 0 ) { - return (int) substr( $d, 8 ); - } - } - - return null; - } -} diff --git a/components/HttpClient/CacheStorage.php b/components/HttpClient/CacheStorage.php deleted file mode 100644 index 5a95584..0000000 --- a/components/HttpClient/CacheStorage.php +++ /dev/null @@ -1,34 +0,0 @@ - Maps URL to temporary body file path during streaming */ - private $body_paths = []; - - public function __construct( Filesystem $fs ) { - $this->fs = $fs; - } - - private function get_body_path( string $url ): string { - $key = hash( 'sha256', $url ); - - return "$key.bin"; - } - - private function get_meta_path( string $url ): string { - $key = hash( 'sha256', $url ); - - return "$key.json"; - } - - public function lookup( string $url ): ?CacheEntry { - $meta_path = $this->get_meta_path( $url ); - $body_path = $this->get_body_path( $url ); - - // Check for metadata first, as body without metadata is useless. - if ( ! $this->fs->exists( $meta_path ) ) { - return null; - } - - // If metadata exists, but body doesn't, invalidate and return null. - if ( ! $this->fs->exists( $body_path ) ) { - $this->invalidate( $url ); - - return null; - } - - $data = json_decode( $this->fs->get_contents( $meta_path ), true ); - $entry = new CacheEntry(); - foreach ( $data as $k => $v ) { - // Skip body_path if it somehow exists in old cache files - if ( $k === 'body_path' ) { - continue; - } - $entry->$k = $v; - } - - // Re-check URL consistency in case of hash collisions (unlikely but possible) - if ( $entry->url !== $url ) { - // Log potential hash collision - $this->invalidate( $url ); // Invalidate the conflicting entry - - return null; - } - - return $entry; - } - - public function open_body_write_stream( string $url ): ByteWriteStream { - $body_path = $this->get_body_path( $url ); - $this->body_paths[ $url ] = $body_path; - - return $this->fs->open_write_stream( $body_path ); - } - - public function get_body( CacheEntry $entry ): string { - $body_path = $this->get_body_path( $entry->url ); - if ( ! $this->fs->exists( $body_path ) ) { - // Invalidate metadata if body is missing - $this->invalidate( $entry->url ); - throw new RuntimeException( "Cache body file not found for URL: {$entry->url}" ); - } - - return $this->fs->get_contents( $body_path ); - } - - public function store( CacheEntry $e ): void { - $meta_path = $this->get_meta_path( $e->url ); - - $jsonData = json_encode( $e, JSON_PRETTY_PRINT ); - if ( json_last_error() !== JSON_ERROR_NONE ) { - throw new Exception( json_last_error_msg() ); - } - $this->fs->put_contents( $meta_path, $jsonData ); - } - - public function invalidate( string $url ): void { - $meta_path = $this->get_meta_path( $url ); - $body_path = $this->get_body_path( $url ); - try { - $this->fs->rm( $meta_path ); - $this->fs->rm( $body_path ); - } catch ( FilesystemException $e ) { - // Ignore - } - // Also remove from temporary tracking if invalidate is called mid-stream - unset( $this->body_paths[ $url ] ); - } -} diff --git a/components/HttpClient/chunked_encoding_server.js b/components/HttpClient/chunked_encoding_server.js deleted file mode 100644 index 2a4d8c1..0000000 --- a/components/HttpClient/chunked_encoding_server.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Use with `http_api.php` to test chunked transfer encoding: - * - * ```php - * $requests = [ - * new Request( "http://127.0.0.1:3000/", [ - * 'http_version' => '1.1' - * ] ), - * new Request( "http://127.0.0.1:3000/", [ - * 'http_version' => '1.0', - * 'headers' => [ - * 'please-redirect' => 'yes', - * ], - * ] ), - * ]; - */ - -const http = require( 'http' ); -const zlib = require( 'zlib' ); - -const server = http.createServer( - ( req, res ) => { - // Check if the client is using HTTP/1.1 - const isHttp11 = req.httpVersion === '1.1'; - res.useChunkedEncodingByDefault = false; - - // Check if the client accepts gzip encoding - const acceptEncoding = req.headers[ 'accept-encoding' ]; - const useGzip = acceptEncoding && acceptEncoding.includes( 'gzip' ); - if ( req.headers[ 'please-redirect' ] ) { - res.writeHead( 301, { Location: req.url } ); - res.end(); - return; - } - // Set headers for chunked transfer encoding if HTTP/1.1 - if ( isHttp11 ) { - res.setHeader( 'Transfer-Encoding', 'chunked' ); - } - - res.setHeader( 'Content-Type', 'text/plain' ); - // Create a function to write chunks - const writeChunks = ( stream ) => { - stream.write( - ` - - - -Chunked transfer encoding test - -` ); - stream.write( '

Chunked transfer encoding test

\n' ); - setTimeout( - () => { - stream.write( '
This is a chunked response after 100 ms.
\n' ); - setTimeout( - () => { - stream.write( - '
This is a chunked response after 1 second. The server should not close the stream before all chunks are sent to a client.
\n\n\n' ); - stream.end(); - }, - 1000, - ); - }, - 100, - ); - }; - if ( useGzip ) { - res.setHeader( 'Content-Encoding', 'gzip' ); - const gzip = zlib.createGzip(); - gzip.pipe( res ); - - if ( isHttp11 ) { - writeChunks( - { - write( data ) { - gzip.write( data ); - gzip.flush(); - }, - end() { - gzip.end(); - }, - }, - ); - } else { - gzip.write( 'Chunked transfer encoding test\n' ); - gzip.write( 'This is a chunked response after 100 ms.\n' ); - gzip.write( 'This is a chunked response after 1 second.\n' ); - gzip.end(); - } - } else { - if ( isHttp11 ) { - writeChunks( res ); - } else { - res.write( 'Chunked transfer encoding test\n' ); - res.write( 'This is a chunked response after 100 ms.\n' ); - res.write( 'This is a chunked response after 1 second.\n' ); - res.end(); - } - } - }, -); - -server.listen( 0, '127.0.0.1', () => { - const newPort = server.address().port; - console.log( `Server is listening on http://127.0.0.1:${ newPort }` ); -} );