# جاوااسکریپت

> کار با API روشا در Node.js 18 به بالا با fetch؛ تابع کمکی با مدیریت 429، سابقهٔ روزانه، هشدار BigInt و رشته برای ins_code و صفحه‌بندی کل سابقه با تاریخ.
> Source: https://poolick.ir/docs/javascript/

در Node.js 18 یا بالاتر، `fetch` داخلی برای کار با روشا کافی است و کتابخانهٔ دیگری لازم نیست. یک تابع `get` کلید را از `ROSHA_KEY` می‌خواند و برای `429` به اندازهٔ `Retry-After` صبر می‌کند. مهم‌ترین نکتهٔ جاوااسکریپت این است که `ins_code` را رشته نگه دارید.

## پیش‌نیاز

Node.js 18 یا بالاتر و کلید در متغیر محیطی. نمونه‌ها `await` سطح بالا دارند؛ آن‌ها را در فایل `.mjs` بگذارید.

```bash
export ROSHA_KEY="rosha_xxxxxxxx_your-secret"
node example.mjs
```

:::caution
کلید را در کد سمت مرورگر نگذارید. این نمونه‌ها برای سرور (Node.js) هستند. در برنامهٔ وب، درخواست را از سرور خودتان بفرستید.
:::

## تابع کمکی `get` چیست؟

`get` یک درخواست `GET` می‌فرستد، اگر `429` گرفت به اندازهٔ `Retry-After` صبر می‌کند و یک بار دیگر می‌فرستد، و برای هر خطای دیگر استثنا می‌دهد.

```js
const BASE = "https://poolick.ir";

async function get(path) {
  for (let attempt = 0; attempt < 2; attempt++) {
    const res = await fetch(BASE + path, {
      headers: { Authorization: `Bearer ${process.env.ROSHA_KEY}` },
    });
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("Retry-After") ?? 1)));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${(await res.json()).detail}`);
    return res.json();
  }
  throw new Error("rate limited");
}

const results = await get("/v1/instruments?q=" + encodeURIComponent("فولاد") + "&limit=3");
console.log(results.map((i) => [i.ins_code, i.ticker, i.name]));
```

مقدار پارامترهای فارسی را با `encodeURIComponent` یا `URLSearchParams` کدگذاری کنید. معنای هر کد خطا در [خطاها](/docs/errors/) است.

## چطور سابقهٔ روزانه را بگیرم؟

`/v1/instruments/{ref}/daily` را صدا بزنید و `rows` را بخوانید. این برنامه ۳۰ روز معاملاتی اخیر فولاد را با قیمت تعدیل‌شده می‌گیرد و بازده بازه را حساب می‌کند:

```js
const BASE = "https://poolick.ir";

async function get(path) {
  for (let attempt = 0; attempt < 2; attempt++) {
    const res = await fetch(BASE + path, {
      headers: { Authorization: `Bearer ${process.env.ROSHA_KEY}` },
    });
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("Retry-After") ?? 1)));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${(await res.json()).detail}`);
    return res.json();
  }
  throw new Error("rate limited");
}

const query = new URLSearchParams({ adjusted: "true", limit: "30" });
const data = await get(`/v1/instruments/46348559193224090/daily?${query}`); // فولاد
const rows = data.rows; // oldest first

const first = rows[0];
const last = rows.at(-1);
console.log(data.ticker, data.ins_code);
console.log(`${first.d} close ${first.close} -> ${last.d} close ${last.close}`);
console.log(`return: ${((last.close / first.close - 1) * 100).toFixed(2)}%`);
```

قیمت‌ها به ریال است. در روزهای بدون معامله `open`، `high` و `low` برابر `null` است؛ پیش از محاسبه آن را بررسی کنید. شرح فیلدها در [سابقهٔ روزانه](/docs/api/daily/) است.

## چرا `ins_code` نباید عدد شود؟

`ins_code` از `Number.MAX_SAFE_INTEGER` (یعنی `9007199254740991`) بزرگ‌تر است و `Number` رقم‌های آخر آن را عوض می‌کند. روشا آن را عمداً به شکل رشته برمی‌گرداند؛ همان رشته را نگه دارید.

```js
const insCode = "32097828799138957"; // شاخص کل, as returned by the API

console.log(String(Number(insCode))); // "32097828799138956" (wrong: the last digit changed)
console.log(Number.isSafeInteger(Number(insCode))); // false

// Correct: keep it as a string everywhere (URLs, Map keys, database columns).
const url = `/v1/indices/${insCode}/daily?limit=1`;
console.log(url);

// Only if you really need numeric ordering, use BigInt, never Number.
console.log(BigInt(insCode) > BigInt("9007199254740991")); // true
```

این خطا بی‌صدا است: برای بعضی کدها عدد درست از آب درمی‌آید و برای بعضی نه. پس آزمودن با یک نماد کافی نیست.

قاعده‌ها:

- `ins_code` را با `Number()`، `parseInt()` یا `+` تبدیل نکنید.
- اگر JSON را با کتابخانه‌ای می‌خوانید که رشته‌های عددی را خودکار عدد می‌کند، این رفتار را برای `ins_code` خاموش کنید.
- `BigInt` در `JSON.stringify` خطا می‌دهد؛ پیش از سریال‌کردن آن را به رشته برگردانید.
- در پایگاه داده، ستون را متنی بگیرید.

## چطور کل سابقهٔ یک نماد را بگیرم؟

هر پاسخ حداکثر ۵٬۰۰۰ ردیف دارد؛ برای سابقهٔ بلندتر با تاریخ صفحه‌بندی کنید. هر بار `end` را روزِ قبل از قدیمی‌ترین ردیف گرفته‌شده بگذارید و وقتی تعداد ردیف‌ها از اندازهٔ صفحه کمتر شد بایستید.

```js
const BASE = "https://poolick.ir";
const PAGE = 5000; // the REST cap on `limit`

async function get(path) {
  for (let attempt = 0; attempt < 2; attempt++) {
    const res = await fetch(BASE + path, {
      headers: { Authorization: `Bearer ${process.env.ROSHA_KEY}` },
    });
    if (res.status === 429) {
      await new Promise((r) => setTimeout(r, 1000 * Number(res.headers.get("Retry-After") ?? 1)));
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${(await res.json()).detail}`);
    return res.json();
  }
  throw new Error("rate limited");
}

function dayBefore(isoDate) {
  const d = new Date(isoDate + "T00:00:00Z");
  d.setUTCDate(d.getUTCDate() - 1);
  return d.toISOString().slice(0, 10);
}

async function fullDaily(ref, adjusted = true) {
  let rows = [];
  let end = null;
  for (;;) {
    const query = new URLSearchParams({ limit: String(PAGE), adjusted: String(adjusted) });
    if (end) query.set("end", end);
    const batch = (await get(`/v1/instruments/${ref}/daily?${query}`)).rows;
    rows = batch.concat(rows); // each page is older than the one before
    if (batch.length < PAGE) return rows;
    end = dayBefore(batch[0].d);
  }
}

const rows = await fullDaily("46348559193224090"); // فولاد
console.log(`${rows.length} rows from ${rows[0].d} to ${rows.at(-1).d}`);
```

همین الگو برای `/client-type`، `/v1/indices/{ref}/daily` و `/v1/funds/{ref}/history` هم کار می‌کند. شرح روش در [قراردادها و قالب‌ها](/docs/conventions/) است.

## نکته‌های دیگر

- **زمان‌ها:** فیلدهای زمان مثل `ts` با اختلاف ساعت تهران (`+03:30`) می‌آیند و `new Date(ts)` آن‌ها را درست می‌خواند. فیلدهای تاریخ مثل `d` فقط روز هستند؛ برای مقایسه همان رشتهٔ ISO کافی است.
- **اعداد بزرگ دیگر:** `value` و `net_asset` عدد اعشاری‌اند و ممکن است از بازهٔ عدد صحیح امن بزرگ‌تر شوند. برای نمایش و محاسبهٔ معمول مشکلی نیست؛ برای حسابداری دقیق تا آخرین ریال، این را در نظر بگیرید.
- **سهمیه:** هر فراخوانی `get` یک درخواست از سقف دقیقه‌ای و سهمیهٔ روزانه کم می‌کند؛ [محدودیت‌ها و پلن‌ها](/docs/limits/).
- **TypeScript:** نوع `ins_code` را `string` تعریف کنید.
