@@ -40,6 +40,62 @@ "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.isTransientDeviceNetworkError = isTransientDeviceNetworkError; | ||
| exports.sendPlaylistToDevice = sendPlaylistToDevice; | ||
| const logger = __importStar(require("../logger")); | ||
| const ff1_compatibility_1 = require("./ff1-compatibility"); | ||
| const SEND_RETRY_ATTEMPTS = 3; | ||
| const SEND_RETRY_BASE_DELAY_MS = 750; | ||
| /** | ||
| * Sleep for a short duration before retrying transient network errors. | ||
| * | ||
| * @param {number} delayMs - Milliseconds to wait | ||
| * @returns {Promise<void>} Promise that resolves after the delay | ||
| */ | ||
| function waitForRetry(delayMs) { | ||
| return new Promise((resolve) => { | ||
| setTimeout(resolve, delayMs); | ||
| }); | ||
| } | ||
| /** | ||
| * isTransientDeviceNetworkError returns true when a send failure is likely temporary. | ||
| * | ||
| * This classifier intentionally targets resolver and route-level failures that are | ||
| * common on local mDNS/Wi-Fi environments. Permanent command errors should surface | ||
| * immediately without retry loops. | ||
| * | ||
| * @param {unknown} error - Error thrown by fetch | ||
| * @returns {boolean} True when retrying is likely to recover | ||
| */ | ||
| function isTransientDeviceNetworkError(error) { | ||
| if (!(error instanceof Error)) { | ||
| return false; | ||
| } | ||
| const networkCodes = new Set([ | ||
| 'EHOSTUNREACH', | ||
| 'ENETUNREACH', | ||
| 'ENOTFOUND', | ||
| 'EAI_AGAIN', | ||
| 'ETIMEDOUT', | ||
| 'ECONNREFUSED', | ||
| 'ECONNRESET', | ||
| ]); | ||
| const message = error.message || ''; | ||
| const messageLooksTransient = message.includes('fetch failed') || | ||
| message.includes('getaddrinfo') || | ||
| message.includes('EHOSTUNREACH') || | ||
| message.includes('ENOTFOUND') || | ||
| message.includes('ETIMEDOUT') || | ||
| message.includes('network timeout') || | ||
| message.includes('No route to host'); | ||
| if (messageLooksTransient) { | ||
| return true; | ||
| } | ||
| const causeCode = error.cause && | ||
| typeof error.cause === 'object' && | ||
| 'code' in error.cause && | ||
| typeof error.cause.code === 'string' | ||
| ? error.cause.code | ||
| : undefined; | ||
| return Boolean(causeCode && networkCodes.has(causeCode)); | ||
| } | ||
| /** | ||
| * Send a DP1 playlist to an FF1 device using the cast API | ||
@@ -123,8 +179,30 @@ * | ||
| } | ||
| // Make the API request | ||
| const response = await fetch(apiUrl, { | ||
| method: 'POST', | ||
| headers, | ||
| body: JSON.stringify(requestBody), | ||
| }); | ||
| // Make the API request with bounded retries for transient local network errors. | ||
| let response = null; | ||
| for (let attempt = 1; attempt <= SEND_RETRY_ATTEMPTS; attempt += 1) { | ||
| try { | ||
| response = await fetch(apiUrl, { | ||
| method: 'POST', | ||
| headers, | ||
| body: JSON.stringify(requestBody), | ||
| }); | ||
| break; | ||
| } | ||
| catch (error) { | ||
| const shouldRetry = attempt < SEND_RETRY_ATTEMPTS && isTransientDeviceNetworkError(error); | ||
| if (!shouldRetry) { | ||
| throw error; | ||
| } | ||
| const retryDelay = SEND_RETRY_BASE_DELAY_MS * attempt; | ||
| logger.warn(`Transient network error while sending playlist (attempt ${attempt}/${SEND_RETRY_ATTEMPTS}): ${error.message}`); | ||
| logger.debug(`Retrying playlist send in ${retryDelay}ms`); | ||
| await waitForRetry(retryDelay); | ||
| } | ||
| } | ||
| if (!response) { | ||
| return { | ||
| success: false, | ||
| error: 'Failed to send playlist to device after retries', | ||
| }; | ||
| } | ||
| // Check response status | ||
@@ -131,0 +209,0 @@ if (!response.ok) { |
+2
-0
@@ -240,2 +240,4 @@ # FF1-CLI Documentation | ||
| # a clear version message before any cast request is sent. | ||
| # The send path also retries transient local-network errors (for example intermittent | ||
| # mDNS/Wi-Fi resolver failures) with a short backoff before returning a final error. | ||
| # Send a hosted DP-1 playlist | ||
@@ -242,0 +244,0 @@ npm run dev -- send "https://cdn.example.com/playlist.json" |
+1
-1
| { | ||
| "name": "ff1-cli", | ||
| "version": "1.0.7", | ||
| "version": "1.0.8", | ||
| "description": "CLI to fetch NFT information and build DP1 playlists using Grok API", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
451985
0.68%9554
0.82%