Spaces:
Running
Running
Download scripts/sync-catalogue.mjs from bep40/VSEARCH: direct link, hf CLI and curl.
- Browser
- Download file 5.41 kB
-
https://huggingface.co/spaces/bep40/VSEARCH/resolve/main/scripts/sync-catalogue.mjs
- Command line
-
hf download hf://spaces/bep40/VSEARCH/scripts/sync-catalogue.mjs
-
curl -L -o sync-catalogue.mjs https://huggingface.co/spaces/bep40/VSEARCH/resolve/main/scripts/sync-catalogue.mjs
5.41 kB
| /** | |
| * Mirror vai-avatar's live product sources into this Space, same-origin. | |
| * | |
| * Why a mirror and not a runtime cross-origin fetch: | |
| * huggingface.co serves dataset files with | |
| * `Access-Control-Allow-Origin: https://huggingface.co`, and `/resolve` then | |
| * 302s to a CDN host that sets its own CORS policy. A page on | |
| * *.static.hf.space is neither origin, so the browser hides the response | |
| * body and the read fails with an opaque "Failed to fetch". Same-origin | |
| * cannot be blocked, so the bytes are copied into the Space instead. | |
| * | |
| * Freshness therefore depends on this script running, not on a rebuild of any | |
| * baked data: re-run it (or let the scheduled job run it) and the Space serves | |
| * whatever vai-avatar has written at that moment. | |
| * | |
| * node scripts/sync-catalogue.mjs | |
| * | |
| * Writes public/catalogue/products.json and public/catalogue/promos.json. | |
| * `public/` is what Vite copies into dist/, so both end up served with the app. | |
| */ | |
| import { mkdir, writeFile } from 'node:fs/promises'; | |
| import { realpathSync } from 'node:fs'; | |
| import { dirname, resolve } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | |
| const OUT_DIR = resolve(ROOT, 'public/catalogue'); | |
| const REVISION = 'main'; | |
| const TIMEOUT_MS = 30_000; | |
| /** Kept byte-identical to the sources declared in src/catalogue.js. */ | |
| export const SOURCES = { | |
| 'products.json': { | |
| repo: 'bep40/grob-products-updated', | |
| file: 'products_url_added.json', | |
| }, | |
| 'promos.json': { | |
| repo: 'bep40/vaistudio-data', | |
| file: 'promos.json', | |
| }, | |
| }; | |
| function urlFor({ repo, file }) { | |
| return `https://huggingface.co/datasets/${repo}/resolve/${REVISION}/${file}?cts=${Date.now()}`; | |
| } | |
| async function fetchSource(name, source) { | |
| const url = urlFor(source); | |
| process.stdout.write(`[sync] ${name} <- ${source.repo}/${source.file} ... `); | |
| const response = await fetch(url, { | |
| cache: 'no-store', | |
| signal: AbortSignal.timeout(TIMEOUT_MS), | |
| headers: process.env.HF_TOKEN ? { Authorization: `Bearer ${process.env.HF_TOKEN}` } : {}, | |
| }); | |
| if (!response.ok) throw new Error(`HTTP ${response.status} for ${url}`); | |
| const text = await response.text(); | |
| // Validate before writing: a truncated or HTML error body would otherwise be | |
| // served to the app as if it were a catalogue. | |
| const parsed = JSON.parse(text); | |
| const count = Array.isArray(parsed) | |
| ? `${parsed.length} products` | |
| : `${Object.keys(parsed?.productEdits ?? {}).length} productEdits`; | |
| process.stdout.write(`ok (${count})\n`); | |
| return text; | |
| } | |
| /** | |
| * Was this file the process entrypoint? | |
| * | |
| * Comparing basenames with `endsWith` looks equivalent and is not: any file whose | |
| * name merely ENDS WITH this one (`my-sync-catalogue.mjs`) also matches, so | |
| * loading such a file would run main() as a side effect of importing it. Both | |
| * paths are resolved before comparing. | |
| */ | |
| function isDirectRun() { | |
| const entry = process.argv[1]; | |
| if (!entry) return false; | |
| try { | |
| return realpathSync(entry) === realpathSync(fileURLToPath(import.meta.url)); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| async function main() { | |
| await mkdir(OUT_DIR, { recursive: true }); | |
| let failures = 0; | |
| for (const [name, source] of Object.entries(SOURCES)) { | |
| try { | |
| const text = await fetchSource(name, source); | |
| await writeFile(resolve(OUT_DIR, name), text, 'utf8'); | |
| } catch (error) { | |
| // The good file is still written, so one transient error does not throw | |
| // away a successful fetch. Whether that mixture may be PUBLISHED is | |
| // decided below. | |
| failures++; | |
| console.error(`[sync] FAILED ${name}: ${error.message}`); | |
| } | |
| } | |
| /** | |
| * ANY failure fails the run, not only a total failure. | |
| * | |
| * This matters from the second successful run onward, once public/catalogue is | |
| * committed: a file that failed to fetch is then still on disk as its previous | |
| * version, so requireRealFile() and recordCount() in refresh-catalogue.mjs both | |
| * pass on that stale copy. The caller would then build, commit and print | |
| * REFRESH_OK, and the Space would serve a fresh products.json beside an old | |
| * promos.json -- so every Sửa and every Xóa from vai-avatar silently stopped | |
| * applying, with no error anywhere. | |
| * | |
| * On the very first run the mirror is not committed yet, so the failed file is | |
| * simply absent and the caller's guard already catches it. Failing here makes | |
| * that behaviour unconditional instead of depending on which run it is. | |
| * | |
| * A stale overlay is not a degraded catalogue, it is a wrong one: catalogue.js | |
| * merges the two files, so old edits resurrect old prices and old tombstones | |
| * fight the new file. Aborting here is what makes the caller leave the | |
| * previously committed mirror intact, which is the guarantee both scripts | |
| * document. | |
| */ | |
| if (failures > 0) { | |
| console.error( | |
| `[sync] ${failures}/${Object.keys(SOURCES).length} source(s) failed — the mirror on disk ` | |
| + 'is now MIXED (fresh file beside last-commit file). Refusing to report success: ' | |
| + 'the caller must abort so the last-good mirror stays live.', | |
| ); | |
| process.exitCode = 1; | |
| return; | |
| } | |
| console.log(`[sync] wrote ${OUT_DIR}`); | |
| } | |
| if (isDirectRun()) { | |
| main().catch(error => { | |
| console.error(`[sync] ${error.stack || error}`); | |
| process.exitCode = 1; | |
| }); | |
| } |