تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

API

واجهات برمجة التطبيقات العامة

إلى جانب المكتبة القياسية لـ ECMAScript، تحتوي بيئة JS في تطبيق NSH على مجموعة من المتغيرات العامة القياسية: fetch مع Headers وRequest وResponse؛ ومعيار Streams؛ وURL وURLSearchParams؛ وTextEncoder / TextDecoder؛ وBlob وFile وFormData؛ وEventTarget وEvent وAbortController؛ والمؤقتات، وrequestAnimationFrame وconsole؛ وIntl لتنسيق الأرقام والتواريخ. وهي مبنية أصلاً بلغة Rust، وتتبع معايير WHATWG، ولا تحتاج إلى أي استيراد، لذلك تعمل المكتبات الكثيرة المبنية على هذه المعايير (عملاء HTTP، ومساعدات URL والترميز، وأدوات التدفقات) دون أي تعديل.

تسرد هذه الصفحة كل المتغيرات العامة المتاحة للتطبيق. وهي خاصة ببيئة TSX: أما شيفرة Rust فتستخدم مكتبة Rust القياسية وcrates.io وخدمات NSH من nsh_core::prelude بدلاً منها (fetch، والمؤقتات، والمهام؛ راجع البناء باستخدام Rust)، وهي مذكورة في عمود Rust من الجدول. تُبنى واجهة المستخدم باستخدام العناصر ومكونات التطبيق نفسه، وكل ما عدا ذلك يأتي من الوحدات التي يستوردها التطبيق.

تُولَّد تصريحات TypeScript لكل ما سبق داخل .nsh/types/nsh/globals.d.ts (راجع بنية المشروع).

المجموعةالمتغيرات العامةRust
وحدة التحكم والتوقيتconsole، performanceحزمة log (log::info!، …)، nsh_core::time::now()
المؤقتات والإطاراتsetTimeout، setInterval، clearTimeout، clearInterval، queueMicrotask، requestAnimationFrame، cancelAnimationFrameset_timeout، set_interval، timer(delay).await، request_animation_frame، next_frame().await، Spawned::cancel()
Base64 والأخطاءatob، btoa، DOMExceptionحزمة base64، Result
الأحداث وإشارات الإلغاءEventTarget، Event، CustomEvent، AbortController، AbortSignalالإشارات (signals) والإغلاقات (closures)؛ Spawned::cancel() تنهي مهمة
عناوين URLURL، URLSearchParamsحزمة url
ترميز النصوصTextEncoder، TextDecoderstr::as_bytes، String::from_utf8
Blob وبيانات النماذجBlob، File، FormDataمحتويات Vec<u8> في HttpRequest
Fetchfetch، Headers، Request، Responsefetch، HttpRequest، HttpResponse
تنسيق الأرقام والتواريخIntl.NumberFormat، Intl.DateTimeFormat، toLocaleStringformat! وحزم التنسيق في crates.io، مع locale()
التدفقاتReadableStream، WritableStream، TransformStream، والقارئات، والكاتبات، والمتحكمات، ByteLengthQueuingStrategy، CountQueuingStrategy، TextEncoderStream، TextDecoderStreamfetch تعيد المحتوى كاملاً؛ وتشغّل مهام background الاتصالات طويلة الأمد على المنصات الأصلية

توفر المضيفات الأصلية أيضاً Buffer وcrypto وprocess وWebSocket؛ راجع الوحدات القياسية لمعرفة وحداتها وأذوناتها وحدود مواردها.

#وحدة التحكم والتوقيت

declare var console: {
  log(...data: unknown[]): void;
  info(...data: unknown[]): void;
  debug(...data: unknown[]): void;
  warn(...data: unknown[]): void;
  error(...data: unknown[]): void;
};

declare var performance: {
  /** Milliseconds since the JS context started, with sub-millisecond precision. */
  now(): number;
};

في Rust، تكتب وحدات الماكرو في حزمة log (log::info!، log::warn!، …) في السجل نفسه، وتظهر أيضاً في طرفية nsh run. تكتب console سطراً واحداً لكل استدعاء في سجل وقت التشغيل (الهدف js): log وinfo بمستوى info، وdebug وwarn وerror بمستوياتها الخاصة. تُضم الوسائط بمسافات؛ وتُطبع السلاسل النصية كما هي، والأخطاء مع تتبع المكدس، والدوال بصيغة [Function name]، والكائنات والمصفوفات بصيغة JSON (مع الرجوع إلى String(value) في حالة الدورات).

وأثناء اتصال جهاز بخادم التطوير، تظهر أسطر سجله في الطرفية التي تشغّل nsh run / nsh dev مسبوقة باسم الجهاز؛ راجع خادم التطوير وتصحيح الأخطاء.

performance.now() هي الساعة التي يمررها requestAnimationFrame إلى دوال الاستدعاء الخاصة به؛ وفي Rust، تمثل nsh_core::time::now() وInstant التي تعيدها request_animation_frame وnext_frame() هذه الساعة نفسها. ليس لـ performance أي أعضاء آخرين.

#المؤقتات والإطارات

function setTimeout(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number;
function setInterval(handler: (...args: any[]) => void, timeout?: number, ...args: any[]): number;
function clearTimeout(id: number | undefined): void;
function clearInterval(id: number | undefined): void;
function queueMicrotask(callback: () => void): void;
/** Runs `callback` at the next display frame with its time in milliseconds. */
function requestAnimationFrame(callback: (time: number) => void): number;
function cancelAnimationFrame(id: number): void;

في Rust: تعيد set_timeout(delay, f) وset_interval(period, f) مقبضاً من النوع Spawned يلغيها عبر cancel()، وتنتظر timer(delay).await داخل مهمة، وتعطي request_animation_frame(f) / next_frame().await وقت الإطار التالي؛ وتنتهي كلها بانتهاء المكوّن الذي أنشأها. تعمل المؤقتات كما في المتصفحات: التأخير المفقود أو السالب أو غير المنتهي يُعامل على أنه 0، والمعرّف المُعاد عدد صحيح موجب، وإلغاء معرّف غير معروف لا يفعل شيئاً. يجب أن يكون handler دالة (لا تُقيَّم معالجات السلاسل النصية). ولا تقيّد المؤقتات التأخيرات المتداخلة بحد أدنى قدره 4 مللي ثانية.

تعمل دوال الاستدعاء الخاصة بـ requestAnimationFrame مرة واحدة، عند الإطار التالي الذي ينتجه المحرك، وكلها بوقت performance.now() نفسه. أما دالة الاستدعاء التي تطلب إطاراً آخر فتُستدعى عند الإطار الذي يليه، وبذلك تعمل الحلقة المعتادة:

spawn(async move {
    let start = next_frame().await;
    loop {
        let time = next_frame().await;
        let elapsed = time.duration_since(start).as_secs_f32() * 1000.0;
        progress.set((elapsed / 300.0).min(1.0));
        if elapsed >= 300.0 {
            break;
        }
    }
});

#Base64 والأخطاء

function atob(data: string): string;
function btoa(data: string): string;

class DOMException extends Error {
  constructor(message?: string, name?: string);
  readonly name: string;
  readonly message: string;
  readonly code: number;
}

تحوّل atob / btoa بين السلاسل الثنائية وBase64 كما في المتصفحات. أما DOMException فهو الخطأ الذي ترفض به العمليات الملغاة: AbortController.abort() بلا سبب يعطي DOMException باسم "AbortError"، وAbortSignal.timeout يعطي واحداً باسم "TimeoutError".

#الأحداث وإشارات الإلغاء

class EventTarget {
  constructor();
  addEventListener(type: string, listener: EventListenerOrEventListenerObject | null,
    options?: AddEventListenerOptions | boolean): void;
  removeEventListener(type: string, listener: EventListenerOrEventListenerObject | null,
    options?: { capture?: boolean } | boolean): void;
  dispatchEvent(event: Event): boolean;
}

interface AddEventListenerOptions {
  capture?: boolean;
  once?: boolean;
  passive?: boolean;
  signal?: AbortSignal;
}

class Event {
  constructor(type: string, init?: { bubbles?: boolean; cancelable?: boolean; composed?: boolean });
  readonly type: string;
  readonly target: EventTarget | null;
  readonly currentTarget: EventTarget | null;
  readonly eventPhase: number; // NONE 0, CAPTURING_PHASE 1, AT_TARGET 2, BUBBLING_PHASE 3
  readonly bubbles: boolean;
  readonly cancelable: boolean;
  readonly defaultPrevented: boolean;
  readonly composed: boolean;
  readonly isTrusted: boolean;
  readonly timeStamp: number;
  returnValue: boolean;
  cancelBubble: boolean;
  readonly srcElement: EventTarget | null; // legacy: `target`
  initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void; // legacy
  composedPath(): EventTarget[];
  preventDefault(): void;
  stopPropagation(): void;
  stopImmediatePropagation(): void;
}

class CustomEvent<T = any> extends Event {
  constructor(type: string, init?: EventInit & { detail?: T });
  readonly detail: T;
}

class AbortController {
  readonly signal: AbortSignal;
  abort(reason?: any): void;
}

class AbortSignal extends EventTarget {
  readonly aborted: boolean;
  readonly reason: any;
  onabort: ((event: Event) => void) | null;
  throwIfAborted(): void;
  static abort(reason?: any): AbortSignal;
  static timeout(milliseconds: number): AbortSignal;
  static any(signals: Iterable<AbortSignal>): AbortSignal;
}

هذه هي أصناف DOM Standard مع اختلاف واحد: لا توجد شجرة، لذلك يُرسَل الحدث إلى هدفه فقط. تعمل مستمعات الالتقاط (capture) أولاً، ثم بقية المستمعات، كما يفعل DOM عند الهدف؛ ولا يؤثر bubbles في شيء. يمكن اشتقاق هذه الأصناف، مما يجعل EventTarget باعث أحداث صغيراً:

class Channel extends EventTarget {
  send(message: string) {
    this.dispatchEvent(new CustomEvent("message", { detail: message }));
  }
}

const channel = new Channel();
channel.addEventListener("message", (e) => console.log((e as CustomEvent<string>).detail));
channel.send("hello");

لا علاقة لهذه الأحداث بأحداث العناصر (onPress، onChange، …)، فهذه الأخيرة خصائص (props)؛ راجع اللمس والإيماءات.

يلغي AbortSignal طلبات fetch وأنابيب التدفقات، ويزيل المستمعات المضافة عبر { signal }. وفي Rust، يعمل الطلب داخل مهمة، وإلغاء المهمة يلغيه:

let search = spawn(async move {
    let _ = fetch("https://api.example.com/search?q=nsh").await;
});
// later, for example when the screen closes (a task also ends with its component):
search.cancel();

#عناوين URL

class URL {
  constructor(url: string | URL, base?: string | URL);
  static canParse(url: string | URL, base?: string | URL): boolean;
  static parse(url: string | URL, base?: string | URL): URL | null;
  /** A `blob:` URL `fetch` reads `blob` from, until revoked. */
  static createObjectURL(blob: Blob): string;
  static revokeObjectURL(url: string): void;
  href: string;
  readonly origin: string;
  protocol: string;
  username: string;
  password: string;
  host: string;
  hostname: string;
  port: string;
  pathname: string;
  search: string;
  hash: string;
  readonly searchParams: URLSearchParams;
  toString(): string;
  toJSON(): string;
}

class URLSearchParams {
  constructor(init?: string | Record<string, string> | Iterable<[string, string]> | URLSearchParams);
  readonly size: number;
  append(name: string, value: string): void;
  delete(name: string, value?: string): void;
  get(name: string): string | null;
  getAll(name: string): string[];
  has(name: string, value?: string): boolean;
  set(name: string, value: string): void;
  sort(): void;
  forEach(callback: (value: string, key: string, parent: URLSearchParams) => void, thisArg?: unknown): void;
  entries(): IterableIterator<[string, string]>;
  keys(): IterableIterator<string>;
  values(): IterableIterator<string>;
  [Symbol.iterator](): IterableIterator<[string, string]>;
  toString(): string;
}

تُحلَّل عناوين URL وفق URL Standard. وخاصية searchParams في عنوان URL حية في الاتجاهين: تغيير المعاملات يعيد كتابة الاستعلام، وتعيين search أو href يعيد تحليله. وتستخدم شيفرة Rust حزمة url، التي تتبع المعيار نفسه:

let mut url = url::Url::parse("https://api.example.com")?.join("/v1/items")?;
url.query_pairs_mut().append_pair("page", "2").append_pair("tag", "new");
url.as_str(); // "https://api.example.com/v1/items?page=2&tag=new"

تتكرر مكررات URLSearchParams (وHeaders وFormData) على لقطة تُؤخذ عند إنشاء المكرر، وفيها دوال مساعدة للمكررات (map، filter، toArray، …).

تعيد URL.createObjectURL(blob) عنوان URL من النوع blob: يمكن لـ fetch (مع GET) قراءته إلى أن يُستدعى revokeObjectURL. ويعرضه <Image> عبر src أو placeholder (Image).

#ترميز النصوص

class TextEncoder {
  readonly encoding: "utf-8";
  encode(input?: string): Uint8Array;
  encodeInto(source: string, destination: Uint8Array): { read: number; written: number };
}

/** UTF-8, UTF-16LE / BE and windows-1252 (with the labels of latin1 and ASCII). */
class TextDecoder {
  constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
  readonly encoding: string;
  readonly fatal: boolean;
  readonly ignoreBOM: boolean;
  decode(input?: BufferSource, options?: { stream?: boolean }): string;
}

يعرف TextDecoder ترميزات أقل مما يعرفه المتصفح: UTF-8 وUTF-16LE وUTF-16BE وwindows-1252 (التي تشير إليها أيضاً التسميات latin1 وiso-8859-1 وascii وما شابهها، كما يحدد Encoding Standard). أما التسميات الأخرى فتُطلق RangeError. ومع { stream: true }، يُحتفظ بالتسلسل متعدد البايتات المقطوع بين جزأين للاستدعاء التالي.

#Blob وبيانات النماذج

class Blob {
  constructor(blobParts?: Iterable<BlobPart>, options?: { type?: string; endings?: "transparent" | "native" });
  readonly size: number;
  readonly type: string;
  slice(start?: number, end?: number, contentType?: string): Blob;
  arrayBuffer(): Promise<ArrayBuffer>;
  bytes(): Promise<Uint8Array>;
  text(): Promise<string>;
  stream(): ReadableStream<Uint8Array>;
}

class File extends Blob {
  constructor(fileBits: Iterable<BlobPart>, fileName: string,
    options?: { type?: string; endings?: "transparent" | "native"; lastModified?: number });
  readonly name: string;
  readonly lastModified: number;
  readonly webkitRelativePath: string;
}

class FormData {
  constructor();
  append(name: string, value: string | Blob, fileName?: string): void;
  delete(name: string): void;
  get(name: string): File | string | null;
  getAll(name: string): (File | string)[];
  has(name: string): boolean;
  set(name: string, value: string | Blob, fileName?: string): void;
  forEach(callback: (value: File | string, key: string, parent: FormData) => void, thisArg?: unknown): void;
  entries(): IterableIterator<[string, File | string]>;
  keys(): IterableIterator<string>;
  values(): IterableIterator<File | string>;
  [Symbol.iterator](): IterableIterator<[string, File | string]>;
}

type BlobPart = ArrayBuffer | ArrayBufferView | Blob | string;

يحتفظ الـ Blob ببايتاته في الذاكرة، وتشاركها slice بدلاً من نسخها. الـ Blob قيم في الذاكرة فقط: لا يوجد وصول إلى نظام الملفات خلف File، ولا يملك FormData دالة بانية من عنصر نموذج.

يُرسَل محتوى FormData بصيغة multipart/form-data، وتحلل Response.formData() / Request.formData() كلاً من multipart/form-data وapplication/x-www-form-urlencoded.

const form = new FormData();
form.append("title", "Holiday");
form.append("photo", new Blob([bytes], { type: "image/jpeg" }), "photo.jpg");
await fetch("https://api.example.com/upload", { method: "POST", body: form });

#Fetch

/**
 * HTTP through the platform's network stack (NSURLSession,
 * HttpURLConnection), streaming both ways; `data:` and `blob:` URLs are
 * answered locally, and `asset:/` URLs with the app's bundled files.
 */
function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;

interface RequestInit {
  method?: string;
  headers?: HeadersInit;
  /** A ReadableStream is sent as it is read (chunked), with `duplex: "half"`. */
  body?: BodyInit | null;
  duplex?: "half";
  signal?: AbortSignal | null;
  /** "manual" gives the 3xx response itself. */
  redirect?: "follow" | "manual" | "error";
  credentials?: "omit" | "same-origin" | "include";
  cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached";
  mode?: "same-origin" | "no-cors" | "cors";
  referrer?: string;
  referrerPolicy?: string;
  integrity?: string;
  keepalive?: boolean;
  priority?: "high" | "low" | "auto";
}

type HeadersInit = Headers | Record<string, string> | Iterable<[string, string]>;
type BodyInit = ReadableStream<Uint8Array> | Blob | BufferSource | FormData | URLSearchParams | string;

يرسل fetch طلبات HTTP وHTTPS عبر حزمة الشبكة الخاصة بالمنصة: NSURLSession على iOS وMac Catalyst، وHttpURLConnection على Android. لذلك تسري إعدادات الوكيل (proxy) في النظام وإعدادات TLS والثقة بالشهادات. أما عناوين data: وعناوين blob: (من URL.createObjectURL) فيُجاب عنها محلياً دون لمس الشبكة؛ وأي مخطط آخر يُرفض. وتمر fetch(request) في Rust عبر الحزمة نفسها والقدرات نفسها، وتقبل عنوان URL أو HttpRequest، وتكتمل بـ HttpResponse يحمل المحتوى كاملاً (الشبكات):

#[derive(serde::Deserialize)]
struct Todo {
    id: u32,
    title: String,
    done: bool,
}

async fn load_todos(token: &str) -> Result<Vec<Todo>, String> {
    let request = HttpRequest::get("https://api.example.com/todos").header("Authorization", format!("Bearer {token}"));
    let response = fetch(request).await?;
    if !response.ok() {
        return Err(format!("HTTP {}", response.status()));
    }
    serde_json::from_slice(&response.body).map_err(|e| e.to_string())
}

#Headers وRequest وResponse

class Headers {
  constructor(init?: HeadersInit);
  append(name: string, value: string): void;
  delete(name: string): void;
  get(name: string): string | null;
  getSetCookie(): string[];
  has(name: string): boolean;
  set(name: string, value: string): void;
  forEach(callback: (value: string, key: string, parent: Headers) => void, thisArg?: unknown): void;
  entries(): IterableIterator<[string, string]>;
  keys(): IterableIterator<string>;
  values(): IterableIterator<string>;
  [Symbol.iterator](): IterableIterator<[string, string]>;
}

interface Body {
  /** The body as it arrives (a byte stream), or null. */
  readonly body: ReadableStream<Uint8Array> | null;
  readonly bodyUsed: boolean;
  arrayBuffer(): Promise<ArrayBuffer>;
  blob(): Promise<Blob>;
  bytes(): Promise<Uint8Array>;
  formData(): Promise<FormData>;
  json(): Promise<any>;
  text(): Promise<string>;
}

class Request implements Body {
  constructor(input: string | URL | Request, init?: RequestInit);
  readonly method: string;
  readonly url: string;
  readonly headers: Headers;
  readonly destination: string;
  readonly referrer: string;
  readonly referrerPolicy: string;
  readonly mode: string;
  readonly credentials: string;
  readonly cache: string;
  readonly redirect: "follow" | "manual" | "error";
  readonly integrity: string;
  readonly keepalive: boolean;
  readonly isReloadNavigation: boolean;
  readonly isHistoryNavigation: boolean;
  readonly signal: AbortSignal;
  readonly duplex: "half";
  clone(): Request;
  // ...and the Body members
}

class Response implements Body {
  constructor(body?: BodyInit | null, init?: { status?: number; statusText?: string; headers?: HeadersInit });
  static json(data: unknown, init?: ResponseInit): Response;
  static error(): Response;
  static redirect(url: string | URL, status?: number): Response;
  readonly type: "basic" | "default" | "error";
  readonly url: string;
  readonly redirected: boolean;
  readonly status: number;
  readonly ok: boolean;
  readonly statusText: string;
  readonly headers: Headers;
  clone(): Response;
  // ...and the Body members
}

#البث

إن body في الاستجابة تدفق بايتات يمتلئ كلما وصلت البيانات من الشبكة، لذلك يمكن معالجة الاستجابات الكبيرة أو طويلة الأمد (التنزيلات، وserver-sent events، ومخرجات النماذج المبثوثة) أثناء وصولها. وعندما ينتظر أكثر من 1 MiB دون قراءة، يُوقَف الطلب مؤقتاً إلى أن يقرأ التطبيق مجدداً.

const res = await fetch("https://api.example.com/events");
const lines = res.body!.pipeThrough(new TextDecoderStream());
for await (const chunk of lines) {
  console.log(chunk);
}

يُرفَع محتوى طلب ReadableStream جزءاً بجزء كلما طلبته الشبكة؛ مرّر duplex: "half" كما يشترط Fetch Standard:

await fetch("https://api.example.com/upload", {
  method: "PUT",
  body: blob.stream(),
  duplex: "half",
});

#الاختلافات عن المتصفحات

  • لا CORS ولا origin. التطبيق ليس صفحة ويب: يمكنه التواصل مع أي مضيف تسمح به قدراته. ويُقبل mode لكن يُتجاهَل.
  • لا ترويسات محظورة. يمكن للتطبيق تعيين Cookie وUser-Agent وOrigin وأي ترويسة أخرى. وعندما لا تُعطى ترويسة Accept، يُرسَل Accept: */*.
  • redirect: "manual" يسلّم استجابة 3xx نفسها، بحالتها وترويسة Location، بدلاً من استجابة معتمة مفلترة. أما "follow" فيتبع عمليات إعادة التوجيه (يُتحقق من كل قفزة مقابل القدرات)؛ وفي "error" يُرفض الطلب عندما تكون الاستجابة إعادة توجيه.
  • محتوى FormData يحصل دائماً على Content-Type: multipart/form-data; boundary=…، حتى لو عُيّنت ترويسة Content-Type. غالباً ما تعيّن مكتبات HTTP نوع محتوى افتراضياً ولا تمسحه إلا عندما تكتشف أنها تعمل في متصفح؛ وبدون الحد (boundary) لا يستطيع الخادم تحليل المحتوى.
  • credentials وcache وreferrer وreferrerPolicy وintegrity وkeepalive وpriority يُتحقق منها وتُحفظ في الكائن Request، لكنها لا تُمرَّر إلى حزمة الشبكة: فالتخزين المؤقت ومعالجة ملفات تعريف الارتباط يتبعان حزمة المنصة.
  • القدرات. عندما يعلن التطبيق قدرات network في nsh.json (أو يمنح التطبيق المضيف قدراً أقل)، فإن الطلب إلى مضيف غير مسموح به لا يصل إلى الشبكة أبداً، ويُرفض fetch بـ TypeError تكون code فيه "ERR_ACCESS_DENIED"، وpermission "network"، وresource عنوان URL المرفوض. راجع العزل والقدرات.
  • الأخطاء. تُرفَض إخفاقات الشبكة بـ TypeError تبدأ رسالته بـ Network request failed:؛ وتُرفَض عمليات الإلغاء بـ reason الخاصة بالإشارة.

راجع الشبكات للاطلاع على دليل تحميل البيانات في المكونات.

#التدفقات

معيار WHATWG Streams كاملاً: التدفقات القابلة للقراءة (الافتراضية وتدفقات البايتات، وقارئات BYOB، وtee، وpipeTo / pipeThrough، وReadableStream.from، والتكرار غير المتزامن)، والتدفقات القابلة للكتابة، وتدفقات التحويل، واستراتيجيتا الطابور، وتدفقات ترميز النصوص.

class ReadableStream<R = any> {
  constructor(underlyingSource: UnderlyingByteSource, strategy?: { highWaterMark?: number });
  constructor(underlyingSource?: UnderlyingDefaultSource<R>, strategy?: QueuingStrategy<R>);
  static from<R>(iterable: Iterable<R | PromiseLike<R>> | AsyncIterable<R>): ReadableStream<R>;
  readonly locked: boolean;
  cancel(reason?: any): Promise<void>;
  getReader(options: { mode: "byob" }): ReadableStreamBYOBReader;
  getReader(): ReadableStreamDefaultReader<R>;
  pipeThrough<T>(transform: { writable: WritableStream<R>; readable: ReadableStream<T> },
    options?: StreamPipeOptions): ReadableStream<T>;
  pipeTo(destination: WritableStream<R>, options?: StreamPipeOptions): Promise<void>;
  tee(): [ReadableStream<R>, ReadableStream<R>];
  values(options?: { preventCancel?: boolean }): AsyncIterableIterator<R>;
  [Symbol.asyncIterator](options?: { preventCancel?: boolean }): AsyncIterableIterator<R>;
}

class ReadableStreamDefaultReader<R = any> {
  constructor(stream: ReadableStream<R>);
  readonly closed: Promise<void>;
  read(): Promise<{ done: false; value: R } | { done: true; value?: R }>;
  releaseLock(): void;
  cancel(reason?: any): Promise<void>;
}

class ReadableStreamBYOBReader {
  constructor(stream: ReadableStream<Uint8Array>);
  readonly closed: Promise<void>;
  read<T extends ArrayBufferView>(view: T, options?: { min?: number }): Promise<ReadableStreamReadResult<T>>;
  releaseLock(): void;
  cancel(reason?: any): Promise<void>;
}

class ReadableStreamDefaultController<R = any> {
  readonly desiredSize: number | null;
  close(): void;
  enqueue(chunk?: R): void;
  error(e?: any): void;
}

class ReadableByteStreamController {
  readonly byobRequest: ReadableStreamBYOBRequest | null;
  readonly desiredSize: number | null;
  close(): void;
  enqueue(chunk: ArrayBufferView): void;
  error(e?: any): void;
}

class ReadableStreamBYOBRequest {
  readonly view: ArrayBufferView | null;
  respond(bytesWritten: number): void;
  respondWithNewView(view: ArrayBufferView): void;
}

class WritableStream<W = any> {
  constructor(underlyingSink?: UnderlyingSink<W>, strategy?: QueuingStrategy<W>);
  readonly locked: boolean;
  abort(reason?: any): Promise<void>;
  close(): Promise<void>;
  getWriter(): WritableStreamDefaultWriter<W>;
}

class WritableStreamDefaultWriter<W = any> {
  constructor(stream: WritableStream<W>);
  readonly closed: Promise<void>;
  readonly desiredSize: number | null;
  readonly ready: Promise<void>;
  abort(reason?: any): Promise<void>;
  close(): Promise<void>;
  releaseLock(): void;
  write(chunk?: W): Promise<void>;
}

class WritableStreamDefaultController {
  readonly signal: AbortSignal;
  error(e?: any): void;
}

class TransformStream<I = any, O = any> {
  constructor(transformer?: Transformer<I, O>, writableStrategy?: QueuingStrategy<I>,
    readableStrategy?: QueuingStrategy<O>);
  readonly readable: ReadableStream<O>;
  readonly writable: WritableStream<I>;
}

class TransformStreamDefaultController<O = any> {
  readonly desiredSize: number | null;
  enqueue(chunk?: O): void;
  error(reason?: any): void;
  terminate(): void;
}

class ByteLengthQueuingStrategy {
  constructor(init: { highWaterMark: number });
  readonly highWaterMark: number;
  readonly size: (chunk: ArrayBufferView) => number;
}

class CountQueuingStrategy {
  constructor(init: { highWaterMark: number });
  readonly highWaterMark: number;
  readonly size: (chunk: any) => 1;
}

class TextEncoderStream {
  readonly encoding: "utf-8";
  readonly readable: ReadableStream<Uint8Array>;
  readonly writable: WritableStream<string>;
}

class TextDecoderStream {
  constructor(label?: string, options?: { fatal?: boolean; ignoreBOM?: boolean });
  readonly encoding: string;
  readonly fatal: boolean;
  readonly ignoreBOM: boolean;
  readonly readable: ReadableStream<string>;
  readonly writable: WritableStream<BufferSource>;
}

كائنات المصدر والمصرف والمحوّل الأساسية (UnderlyingDefaultSource، UnderlyingByteSource، UnderlyingSink، Transformer) وStreamPipeOptions (preventAbort، preventCancel، preventClose، signal) لها الأشكال القياسية؛ وتصريحاتها الكاملة في globals.d.ts.

يُبقي TextEncoderStream الزوج البديل (surrogate pair) المقطوع بين جزأين نصيين معاً، ويفعل TextDecoderStream ذلك مع التسلسل متعدد البايتات المقطوع بين جزأين من البايتات.

تدفق تحويل يقسّم النص إلى أسطر:

function lines(): TransformStream<string, string> {
  let rest = "";
  return new TransformStream({
    transform(chunk, controller) {
      const parts = (rest + chunk).split("\n");
      rest = parts.pop()!;
      for (const line of parts) controller.enqueue(line);
    },
    flush(controller) {
      if (rest) controller.enqueue(rest);
    },
  });
}

const res = await fetch("https://api.example.com/log");
for await (const line of res.body!.pipeThrough(new TextDecoderStream()).pipeThrough(lines())) {
  console.log(line);
}

#تنسيق الأرقام والتواريخ

لا يملك QuickJS كائن Intl خاصاً به؛ وتضيف NSH واحداً يجري التنسيق فيه بلغة Rust: Intl.NumberFormat وIntl.DateTimeFormat وIntl.getCanonicalLocales، إضافة إلى toLocaleString للأرقام وأعداد BigInt، وtoLocaleString / toLocaleDateString / toLocaleTimeString للتواريخ، وهذه تقبل locales وoptions نفسيهما. تصريحاتها هي تصريحات TypeScript نفسها (مكتبة ES2023)، لذلك ليست في globals.d.ts.

(1234567.891).toLocaleString();                                              // "1,234,567.891" (en-US)
(1234.5).toLocaleString("de-DE", { style: "currency", currency: "EUR" });  // "1.234,50 €"
new Intl.NumberFormat("en", { notation: "compact" }).format(1_500_000);      // "1.5M"
new Intl.NumberFormat("en", { style: "percent", signDisplay: "exceptZero" }).format(0.042); // "+4%"
new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false });   // "09:05"
new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeStyle: "short" }).format(date); // "Oct 16, 2026, 3:04 PM"

بدون locales، تُستخدم لغة الجهاز: تنسيق منطقته (iOS NSLocale.currentLocale، Android Locale.getDefault())، وen-US في nsh test وعلى الويب.

  • الأرقام: الأنماط decimal وpercent وcurrency (currencyDisplay symbol، narrowSymbol، code) وunit (الوحدات الشائعة: الأطوال، والكتل، والمدد، وأحجام البيانات، ودرجات الحرارة، والسرعات)؛ وminimumIntegerDigits، وminimum / maximumFractionDigits، وminimum / maximumSignificantDigits، وroundingPriority، وtrailingZeroDisplay، وuseGrouping، وsignDisplay؛ والتدوين standard وscientific وengineering وcompact (compactDisplay short / long). التقريب يكون بعيداً عن الصفر عند المنتصف، على الرقم كما كُتب (1.005 يُقرَّب إلى 1.01، كما في المتصفحات). كما يقبل format أعداد BigInt والسلاسل العشرية، بدقة تامة. ويعمل formatToParts وresolvedOptions كما في المتصفحات.
  • التواريخ: weekday وyear وmonth (numeric و2-digit وshort وlong وnarrow) وday وhour وminute وsecond وfractionalSecondDigits وhour12 / hourCycle وtimeZoneName وdateStyle / timeStyle، في التقويم الميلادي. يأخذ timeZone منطقة الجهاز الزمنية (وفي المتصفح، منطقة المتصفح)، أو UTC أو إزاحة ("+08:00")؛ أما أسماء المناطق الأخرى فتُطلق RangeError (لا توجد قاعدة بيانات للمناطق الزمنية).
  • اللغات المحلية: بيانات اللغات جدول للغات الشائعة (الإنجليزية، والألمانية، والفرنسية، والإسبانية، والإيطالية، والبرتغالية، والهولندية، والروسية، والسويدية، والبولندية، والتركية، والإندونيسية، والفيتنامية، والتايلاندية، والهندية، والصينية، واليابانية، والكورية) مع متغيراتها الإقليمية (en-GB، en-IN، de-CH، es-MX، pt-PT، zh-TW، …)، وليست CLDR بأكملها، لذلك قد تختلف التفاصيل عن مخرجات المتصفح. وإذا لم تتوفر بيانات للغة، يُرجَع إلى اللغة التالية المطلوبة، ثم إلى لغة الجهاز، ثم en-US؛ وتواريخ الروسية والسويدية والبولندية والتركية والإندونيسية والفيتنامية والتايلاندية والهندية ليس لها أسماء أشهر (تُكتب أشهرها أرقاماً، وأيام أسبوعها بالإنجليزية). الأرقام لاتينية دائماً.
  • غير الموجود: Intl.Collator (String.prototype.localeCompare تابعة لـ QuickJS، وتتجاهل اللغة المحلية)، وPluralRules، وRelativeTimeFormat، وListFormat، وDisplayNames، وSegmenter، وIntl.Locale؛ ويُنسَّق currencySign: "accounting" وcurrencyDisplay: "name" بصيغة standard وcode.

#الأداء

كل هذه الأصناف أصلية: تحليل عناوين URL، وترميز النصوص وفك ترميزها، وبناء محتويات multipart، ونقل أجزاء التدفقات، كلها تجري في Rust لا في QuickJS. ومع ذلك، فكل استدعاء من JS يعبر إلى وقت التشغيل، لذلك فضّل العمليات على المخزن المؤقت كاملاً (res.json()، res.text()، encoder.encode(s)) على الحلقات بايتاً بايتاً في JS. راجع الأداء.