$value ) { if ( is_array( $value ) ) { // Nested arrays (e.g. multi-value filters): recurse. $out[ $key ] = mlsimport_live_cache_normalize( $value ); } elseif ( is_float( $value ) || ( is_string( $value ) && is_numeric( $value ) && false !== strpos( $value, '.' ) ) ) { // A real float, or a numeric string carrying a decimal point (map // coords arrive as strings): round to 3dp so tiny pan deltas match. $out[ $key ] = round( (float) $value, 3 ); } else { // Ints, bools, plain strings: keep verbatim. $out[ $key ] = $value; } } // Sort by key so param order never changes the resulting key. ksort( $out ); return $out; } /** * Return the cached value for $key, refreshing it via $fetch when stale. * * $fetch returning null means "the source failed" — the stale payload (when * one exists) is served instead and retried on the next request. * * @param string $key Cache key from mlsimport_live_cache_key(). * @param callable $fetch Returns the fresh value, or null on failure. * @return mixed Null only when there is no fresh value and no stale copy. */ function mlsimport_live_remember( string $key, callable $fetch ) { // Load any existing entry and pin "now" for the freshness comparison. $entry = get_transient( $key ); $now = time(); // Fresh hit: a well-formed entry still inside its freshness window — return // the payload without touching the source. if ( is_array( $entry ) && array_key_exists( 'data', $entry ) && isset( $entry['fresh_until'] ) && $entry['fresh_until'] >= $now ) { return $entry['data']; } // Miss or stale: go to the source. $fresh = $fetch(); if ( null !== $fresh ) { // Success: store payload + a fresh-until deadline, but let the whole // row live a full day so it can still be served warm-stale after. set_transient( $key, array( 'data' => $fresh, 'fresh_until' => $now + mlsimport_live_cache_ttl(), ), DAY_IN_SECONDS ); return $fresh; } // Source failed: serve warm-stale when we have it. if ( is_array( $entry ) && array_key_exists( 'data', $entry ) ) { return $entry['data']; } // No fresh value and no stale copy: the caller gets nothing. return null; } /** * Retire every live-mode cache entry (the settings screen button). * * Keyed entries are invalidated by bumping the generation salt. The handful * of fixed-name transients (the entitlement flag, price ceiling, provider * tokens) are deleted directly — the API also evicts them from persistent * object caches. * * @return void */ function mlsimport_live_cache_clear(): void { // Bump the generation salt: every keyed entry now hashes to a new key and // is effectively retired (the orphans expire with their day TTL). update_option( 'mlsimport_live_cache_version', mlsimport_live_cache_version() + 1 ); // Fixed-name shared transients aren't keyed by the salt, so delete them by // hand. The Provider Family module owns the provider-token list. $fixed = array( 'mlsimport_live_entitlement_checked', 'mlsimport_live_price_ceiling' ); // Delete each — this also evicts it from a persistent object cache. foreach ( $fixed as $name ) { delete_transient( $name ); } Mlsimport_Provider_Family::clear_direct_access_tokens(); }