// Node.js 18+. Run only on your server, with BLOOMHUB_API_KEY in its environment. import {pathToFileURL} from 'node:url'; const base = 'https://bloomhub.ru/api/v1'; const pause = ms => new Promise(resolve => setTimeout(resolve, ms)); function waitTime(header, attempt) { if (header) { const seconds = Number(header); const milliseconds = Number.isFinite(seconds) ? seconds * 1000 : Date.parse(header) - Date.now(); if (Number.isFinite(milliseconds)) return Math.max(1000, milliseconds); } return 500 * 2 ** attempt; } export async function apiGet(path, query = {}) { if (!['/me','/locations','/products','/recipes','/stock','/orders'].includes(path)) { throw new Error('This example supports only documented list methods.'); } const key = process.env.BLOOMHUB_API_KEY; if (!key) throw new Error('Set BLOOMHUB_API_KEY on your server.'); const url = new URL(base + path); for (const [name, value] of Object.entries(query)) { if (value !== undefined && value !== '') url.searchParams.set(name, String(value)); } for (let attempt = 0; attempt < 5; attempt++) { let response; try { response = await fetch(url, { headers: {Authorization: `Bearer ${key}`, Accept: 'application/json'}, signal: AbortSignal.timeout(15_000), }); } catch (error) { if (attempt === 4) throw error; await pause(waitTime(null, attempt)); continue; } if ((response.status === 429 || response.status >= 500) && attempt < 4) { await response.body?.cancel(); await pause(waitTime(response.headers.get('Retry-After'), attempt)); continue; } const body = await response.json(); if (!response.ok) { const code = typeof body.error === 'object' ? body.error?.code : body.code; throw new Error(`BloomHub HTTP ${response.status}: ${code || 'REQUEST_FAILED'}`); } return body; } throw new Error('No response after retry limit.'); } export async function* apiPages(path, query = {}) { if (!['/products','/recipes','/stock','/orders'].includes(path)) { throw new Error('Pagination applies to products, recipes, stock and orders.'); } for (let page = 1; page <= 10000; page++) { const data = await apiGet(path, {...query, page, limit: 100}); if (!Array.isArray(data.items) || typeof data.pagination?.hasMore !== 'boolean') { throw new Error('Unexpected response. Do not advance the synchronization watermark.'); } yield* data.items; if (!data.pagination.hasMore) return; } throw new Error('Page limit reached. Do not mark a partial sync complete.'); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const resource = '/' + (process.env.BLOOMHUB_RESOURCE || 'products').replace(/^\/+/, ''); const query = { locationId: process.env.BLOOMHUB_LOCATION_ID, updatedAfter: process.env.BLOOMHUB_SINCE, }; try { for await (const item of apiPages(resource, query)) console.log(JSON.stringify(item)); } catch (error) { console.error(error.message); process.exitCode = 1; } }