Spaces:
Running
Running
File size: 6,476 Bytes
78948ff 3e8ea5d 32151b6 84810e9 805101e 78948ff e445b51 96bdf6c e445b51 78948ff 32151b6 78948ff 96bdf6c 78948ff 96bdf6c 78948ff 96bdf6c 78948ff 96bdf6c 78948ff 96bdf6c 78948ff e445b51 78948ff 3e8ea5d e445b51 96bdf6c 78948ff e445b51 96bdf6c e445b51 96bdf6c e445b51 78948ff e445b51 78948ff e445b51 78948ff 805101e 78948ff 96bdf6c 78948ff 96bdf6c 78948ff e445b51 96bdf6c e445b51 96bdf6c 78948ff e445b51 78948ff 96bdf6c e445b51 78948ff e445b51 96bdf6c e445b51 96bdf6c e445b51 96bdf6c e445b51 78948ff e445b51 78948ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | #!/usr/bin/env node
import { readPositiveIntegerEnv } from './env-utils.mjs';
import { validateSpaceUrl } from './hf-space-doctor-utils.mjs';
const DEFAULT_SPACE_URL = 'https://misonl-visual-journal.hf.space';
const KEEPALIVE_USER_AGENT = 'visual-journal-keepalive/1.0';
const DEFAULT_KEEPALIVE_PATH = '/api/auth-status';
const DEFAULT_TIMEOUT_MS = 30_000;
const DEFAULT_MAX_ATTEMPTS = 1;
const DEFAULT_RETRY_DELAY_MS = 5_000;
const DEFAULT_RETRY_MAX_DELAY_MS = 30_000;
function normalizeUrl(rawUrl, path) {
const urlError = validateSpaceUrl(rawUrl);
if (urlError) {
throw new Error(urlError.replace('HF Space URL', 'HF_SPACE_KEEPALIVE_URL'));
}
const baseUrl = new URL(rawUrl);
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return new URL(normalizedPath, baseUrl).toString();
}
function readExpectedPasswordRequired() {
const rawValue = process.env.HF_SPACE_KEEPALIVE_EXPECT_PASSWORD_REQUIRED?.trim();
if (!rawValue) return undefined;
if (rawValue === 'true') return true;
if (rawValue === 'false') return false;
throw new Error('HF_SPACE_KEEPALIVE_EXPECT_PASSWORD_REQUIRED must be true or false');
}
function readResponseContentType(response) {
return response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() || 'unknown';
}
async function readJsonResponse(response) {
const contentType = readResponseContentType(response);
const text = await response.text();
if (!text) return { body: undefined, contentType, bodyType: 'empty' };
try {
return { body: JSON.parse(text), contentType, bodyType: 'json' };
} catch {
return { body: undefined, contentType, bodyType: 'non-json' };
}
}
function readKeepaliveConfig() {
const spaceUrl = process.env.HF_SPACE_KEEPALIVE_URL?.trim() || DEFAULT_SPACE_URL;
const path = process.env.HF_SPACE_KEEPALIVE_PATH?.trim() || DEFAULT_KEEPALIVE_PATH;
const timeoutMs = readPositiveIntegerEnv('HF_SPACE_KEEPALIVE_TIMEOUT_MS', DEFAULT_TIMEOUT_MS, 1_000);
const maxAttempts = readPositiveIntegerEnv('HF_SPACE_KEEPALIVE_MAX_ATTEMPTS', DEFAULT_MAX_ATTEMPTS);
const retryDelayMs = readPositiveIntegerEnv('HF_SPACE_KEEPALIVE_RETRY_DELAY_MS', DEFAULT_RETRY_DELAY_MS);
const retryMaxDelayMs = readPositiveIntegerEnv(
'HF_SPACE_KEEPALIVE_RETRY_MAX_DELAY_MS',
Math.max(DEFAULT_RETRY_MAX_DELAY_MS, retryDelayMs),
retryDelayMs
);
const expectedPasswordRequired = readExpectedPasswordRequired();
const url = normalizeUrl(spaceUrl, path);
return { url, timeoutMs, maxAttempts, retryDelayMs, retryMaxDelayMs, expectedPasswordRequired };
}
function formatKeepaliveError(error, timeoutMs) {
if (error?.name === 'AbortError') {
return `Keepalive request timed out after ${timeoutMs}ms`;
}
return error instanceof Error ? error.message : String(error);
}
async function waitBeforeRetry(ms) {
await new Promise((resolve) => setTimeout(resolve, ms));
}
function getRetryDelayMs(config, completedAttempts) {
const multiplier = 2 ** Math.min(completedAttempts - 1, 30);
return Math.min(config.retryDelayMs * multiplier, config.retryMaxDelayMs);
}
async function pingKeepaliveEndpointOnce(config, attemptLabel) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
const startedAt = Date.now();
try {
const response = await fetch(config.url, {
headers: {
'User-Agent': KEEPALIVE_USER_AGENT
},
signal: controller.signal
});
const elapsedMs = Date.now() - startedAt;
const result = await readJsonResponse(response);
if (!response.ok) {
throw new Error(
`Keepalive endpoint failed with HTTP ${response.status} (${result.contentType}; ${result.bodyType} body)`
);
}
if (result.bodyType === 'non-json') {
throw new Error(`Keepalive endpoint returned non-JSON body (${result.contentType})`);
}
if (
config.expectedPasswordRequired !== undefined &&
result.body?.passwordRequired !== config.expectedPasswordRequired
) {
throw new Error(
`passwordRequired expected ${config.expectedPasswordRequired}, got ${result.body?.passwordRequired}`
);
}
console.log(
JSON.stringify(
{
ok: true,
url: config.url,
status: response.status,
elapsedMs,
contentType: result.contentType,
passwordRequired: result.body?.passwordRequired,
attempt: attemptLabel
},
null,
2
)
);
} finally {
clearTimeout(timeout);
}
}
async function pingKeepaliveEndpoint() {
const config = readKeepaliveConfig();
let lastError;
for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
try {
const attemptLabel = `${attempt}/${config.maxAttempts}`;
await pingKeepaliveEndpointOnce(config, attemptLabel);
return;
} catch (error) {
lastError = error;
const nextRetryDelayMs =
attempt < config.maxAttempts ? getRetryDelayMs(config, attempt) : undefined;
console.error(
JSON.stringify(
{
ok: false,
attempt: `${attempt}/${config.maxAttempts}`,
error: formatKeepaliveError(error, config.timeoutMs),
...(nextRetryDelayMs === undefined ? {} : { nextRetryDelayMs })
},
null,
2
)
);
if (nextRetryDelayMs !== undefined) {
await waitBeforeRetry(nextRetryDelayMs);
}
}
}
throw new Error(`Keepalive attempt already reported: ${formatKeepaliveError(lastError, config.timeoutMs)}`);
}
pingKeepaliveEndpoint().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
if (!message.startsWith('Keepalive attempt already reported: ')) {
console.error(JSON.stringify({ ok: false, error: message }, null, 2));
}
process.exit(1);
});
|