Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

API

APIs globais

Além da biblioteca padrão do ECMAScript, o ambiente JS de um app NSH tem um conjunto de globais padrão: fetch com Headers, Request e Response; o Streams Standard; URL e URLSearchParams; TextEncoder / TextDecoder; Blob, File e FormData; EventTarget, Event e AbortController; timers, requestAnimationFrame e console; e Intl para formatação de números e datas. Elas são implementadas nativamente em Rust, seguem os padrões WHATWG e não precisam de import, de modo que as muitas bibliotecas JavaScript construídas sobre esses padrões (clientes HTTP, auxiliares de URL e de codificação, utilitários de stream) funcionam sem alterações.

Esta página lista todas as globais que um app tem. Elas são as do ambiente TSX: o código Rust usa a biblioteca padrão do Rust, o crates.io e os serviços do NSH de nsh_core::prelude (fetch, timers, tarefas; veja Desenvolvimento com Rust), indicados na coluna Rust da tabela. A UI é construída com elementos e os componentes do próprio app, e tudo o mais que um app usa vem dos módulos que ele importa.

As declarações TypeScript de todas elas são geradas em .nsh/types/nsh/globals.d.ts (veja Estrutura do projeto).

Grupo Globais Rust
Console e medição de tempo console, performance o crate log (log::info!, …), nsh_core::time::now()
Timers e frames setTimeout, setInterval, clearTimeout, clearInterval, queueMicrotask, requestAnimationFrame, cancelAnimationFrame set_timeout, set_interval, timer(delay).await, request_animation_frame, next_frame().await, Spawned::cancel()
Base64 e erros atob, btoa, DOMException o crate base64, Result
Eventos e abort signals EventTarget, Event, CustomEvent, AbortController, AbortSignal signals e closures; Spawned::cancel() encerra uma tarefa
URLs URL, URLSearchParams o crate url
Codificação de texto TextEncoder, TextDecoder str::as_bytes, String::from_utf8
Blobs e form data Blob, File, FormData corpos Vec<u8> de HttpRequest
Fetch fetch, Headers, Request, Response fetch, HttpRequest, HttpResponse
Formatação de números e datas Intl.NumberFormat, Intl.DateTimeFormat, toLocaleString format! e os crates de formatação do crates.io, com locale()
Streams ReadableStream, WritableStream, TransformStream, readers, writers, controllers, ByteLengthQueuingStrategy, CountQueuingStrategy, TextEncoderStream, TextDecoderStream fetch entrega o corpo inteiro; tarefas background executam conexões de longa duração em alvos nativos

Os hosts nativos também fornecem Buffer, crypto, process e WebSocket; veja Módulos padrão para seus módulos, permissões e limites de recursos.

#Console e medição de tempo

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;
};

Em Rust, as macros do crate log (log::info!, log::warn!, …) escrevem no mesmo log e também aparecem no terminal de nsh run. console escreve uma linha por chamada no log do runtime (target js): log e info no nível info, debug, warn e error em seus respectivos níveis. Os argumentos são unidos por espaços; strings são impressas literalmente, erros com seu stack, funções como [Function name], e objetos e arrays como JSON (recorrendo a String(value) em caso de ciclos).

Enquanto um dispositivo está conectado ao servidor de desenvolvimento, suas linhas de log aparecem no terminal que executa nsh run / nsh dev, prefixadas com o nome do dispositivo; veja Servidor de desenvolvimento e depuração.

performance.now() é o relógio que requestAnimationFrame passa para seus callbacks; em Rust, nsh_core::time::now() e os Instants que request_animation_frame e next_frame() fornecem são esse relógio. performance não tem outros membros.

#Timers e frames

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) e set_interval(period, f) retornam um handle Spawned cujo cancel() os cancela, timer(delay).await espera em uma tarefa, e request_animation_frame(f) / next_frame().await fornecem o tempo do próximo frame; todos terminam junto com o componente que os criou. Os timers se comportam como nos navegadores: um atraso ausente, negativo ou não finito é 0, o id retornado é um inteiro positivo, e cancelar um id desconhecido não faz nada. handler deve ser uma função (handlers em string não são avaliados). Os timers não limitam atrasos aninhados a 4 ms.

Os callbacks de requestAnimationFrame são executados uma vez, no próximo frame que o engine produzir, todos com o mesmo tempo performance.now(). Um callback que solicita outro frame é chamado no frame seguinte, de modo que o loop usual funciona:

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 e erros

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 convertem entre strings binárias e Base64 como nos navegadores. DOMException é com o que as operações abortadas são rejeitadas: um AbortController.abort() sem motivo gera um DOMException chamado "AbortError", e AbortSignal.timeout um chamado "TimeoutError".

#Eventos e abort signals

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;
}

Estas são as classes do DOM Standard com uma diferença: não há árvore, então um evento é despachado apenas para o seu alvo. Os listeners de captura são executados primeiro, depois os demais, como o DOM faz no alvo; bubbles não tem efeito. As classes podem ser estendidas por subclasses, o que torna EventTarget um pequeno emissor de eventos:

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");

Esses eventos não têm relação com os eventos dos elementos (onPress, onChange, …), que são props; veja Toque e gestos.

AbortSignal cancela requisições fetch e pipes de stream, e remove listeners adicionados com { signal }. Em Rust, uma requisição é executada em uma tarefa, e cancelar a tarefa a cancela:

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();

#URLs

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;
}

As URLs são analisadas conforme o URL Standard. O searchParams de uma URL é ativo nas duas direções: alterar os parâmetros reescreve a query, e definir search ou href a analisa de novo. O código Rust usa o crate url, que segue o mesmo padrão:

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"

Os iteradores de URLSearchParams (e de Headers e FormData) iteram sobre um snapshot feito quando o iterador foi criado, e têm os iterator helpers (map, filter, toArray, …).

URL.createObjectURL(blob) retorna uma URL blob: que fetch (com GET) pode ler até que revokeObjectURL seja chamado. <Image> exibe uma como seu src ou placeholder (Image).

#Codificação de texto

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 conhece menos codificações que um navegador: UTF-8, UTF-16LE, UTF-16BE e windows-1252 (que os rótulos latin1, iso-8859-1, ascii e semelhantes também nomeiam, como o Encoding Standard especifica). Outros rótulos lançam um RangeError. Com { stream: true }, uma sequência multibyte cortada entre dois chunks é mantida para a próxima chamada.

#Blobs e form data

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;

Um blob mantém seus bytes na memória, e slice os compartilha em vez de copiá-los. Blobs são apenas valores em memória: não há acesso ao sistema de arquivos por trás de File, e FormData não tem construtor a partir de um elemento de formulário.

Um corpo FormData é enviado como multipart/form-data, e Response.formData() / Request.formData() analisam tanto multipart/form-data quanto 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 envia requisições HTTP e HTTPS pela pilha de rede da própria plataforma: NSURLSession no iOS e no Mac Catalyst, HttpURLConnection no Android. Assim, as configurações de proxy do sistema, a configuração de TLS e a confiança em certificados se aplicam. URLs data: e URLs blob: (de URL.createObjectURL) são respondidas localmente sem tocar na rede; qualquer outro esquema é rejeitado. O fetch(request) do Rust passa pela mesma pilha e pelas mesmas capabilities, recebe uma URL ou um HttpRequest, e resolve com um HttpResponse contendo o corpo inteiro (Rede):

#[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 e 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
}

#Streaming

O body de uma resposta é um stream de bytes preenchido à medida que a rede entrega os dados, de modo que respostas grandes ou de longa duração (downloads, server-sent events, saída de modelos em streaming) podem ser processadas enquanto chegam. Quando mais de 1 MiB está esperando sem ser lido, a requisição é pausada até que o app volte a ler.

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);
}

Um corpo de requisição ReadableStream é enviado chunk a chunk conforme a rede o solicita; passe duplex: "half" como o Fetch Standard exige:

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

#Diferenças em relação aos navegadores

  • Sem CORS e sem origin. Um app não é uma página web: ele pode se comunicar com qualquer host que suas capabilities permitam. mode é aceito e ignorado.
  • Sem headers proibidos. Um app pode definir Cookie, User-Agent, Origin e qualquer outro header. Quando nenhum header Accept é informado, Accept: */* é enviado.
  • redirect: "manual" entrega a própria resposta 3xx, com seu status e o header Location, em vez de uma resposta filtrada opaca. "follow" segue redirecionamentos (cada salto é verificado contra as capabilities); "error" rejeita quando a resposta é um redirecionamento.
  • Um corpo FormData sempre recebe Content-Type: multipart/form-data; boundary=…, mesmo quando um header Content-Type foi definido. Bibliotecas HTTP frequentemente definem um content type padrão e o removem apenas onde detectam um navegador; sem o boundary, o servidor não consegue analisar o corpo.
  • credentials, cache, referrer, referrerPolicy, integrity, keepalive e priority são validados e mantidos no objeto Request, mas não são passados à pilha de rede: o cache e o tratamento de cookies são os da pilha da plataforma.
  • Capabilities. Quando o app declara capabilities network em nsh.json (ou o app hospedeiro concede menos), uma requisição a um host não permitido nunca chega à rede e fetch rejeita com um TypeError cujo code é "ERR_ACCESS_DENIED", permission "network" e resource a URL recusada. Veja Sandbox e capabilities.
  • Erros. Falhas de rede rejeitam com um TypeError cuja mensagem começa com Network request failed:; cancelamentos rejeitam com o reason do signal.

Veja Rede para um guia de carregamento de dados em componentes.

#Streams

O Streams Standard completo do WHATWG: readable streams (default e byte streams, BYOB readers, tee, pipeTo / pipeThrough, ReadableStream.from, iteração assíncrona), writable streams, transform streams, as duas estratégias de enfileiramento e os streams de codificação de texto.

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>;
}

Os objetos underlying source, sink e transformer (UnderlyingDefaultSource, UnderlyingByteSource, UnderlyingSink, Transformer) e StreamPipeOptions (preventAbort, preventCancel, preventClose, signal) têm os formatos padrão; suas declarações completas estão em globals.d.ts.

TextEncoderStream mantém junto um surrogate pair cortado entre dois chunks de string, e TextDecoderStream uma sequência multibyte cortada entre dois chunks de bytes.

Um transform stream que divide texto em linhas:

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);
}

#Formatação de números e datas

O QuickJS não tem um Intl próprio; o NSH adiciona um, formatando em Rust: Intl.NumberFormat, Intl.DateTimeFormat e Intl.getCanonicalLocales, e o toLocaleString de números e BigInts e toLocaleString / toLocaleDateString / toLocaleTimeString de datas, que recebem os mesmos locales e options. Suas declarações são as do próprio TypeScript (a biblioteca ES2023), portanto não estão em 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"

Sem locales, o locale do dispositivo é usado: seu formato de região (iOS NSLocale.currentLocale, Android Locale.getDefault()), en-US em nsh test e na web.

  • Números: os estilos decimal, percent, currency (currencyDisplay symbol, narrowSymbol, code) e unit (unidades comuns: comprimentos, massas, durações, tamanhos de dados, temperaturas, velocidades); minimumIntegerDigits, minimum / maximumFractionDigits, minimum / maximumSignificantDigits, roundingPriority, trailingZeroDisplay, useGrouping, signDisplay; notação standard, scientific, engineering e compact (compactDisplay short / long). O arredondamento é para longe do zero em caso de metade, sobre o número como escrito (1.005 arredonda para 1.01, como nos navegadores). format também aceita BigInts e strings decimais, com exatidão. formatToParts e resolvedOptions funcionam como nos navegadores.
  • Datas: weekday, year, month (numeric, 2-digit, short, long, narrow), day, hour, minute, second, fractionalSecondDigits, hour12 / hourCycle, timeZoneName e dateStyle / timeStyle, no calendário gregoriano. timeZone assume o fuso do dispositivo (em um navegador, o do navegador), UTC ou um offset ("+08:00"); outros nomes de fuso lançam um RangeError (não há banco de dados de fusos horários).
  • Locales: os dados de locale são uma tabela de idiomas comuns (inglês, alemão, francês, espanhol, italiano, português, holandês, russo, sueco, polonês, turco, indonésio, vietnamita, tailandês, hindi, chinês, japonês, coreano) com suas variantes regionais (en-GB, en-IN, de-CH, es-MX, pt-PT, zh-TW, …), e não o CLDR inteiro, portanto os detalhes podem diferir da saída de um navegador. Um locale sem dados recorre ao próximo solicitado, depois ao do dispositivo e então a en-US; as datas em russo, sueco, polonês, turco, indonésio, vietnamita, tailandês e hindi não têm nomes de meses (os meses são escritos como números e os dias da semana em inglês). Os dígitos são sempre latinos.
  • Não existem: Intl.Collator (String.prototype.localeCompare é o do QuickJS, que ignora o locale), PluralRules, RelativeTimeFormat, ListFormat, DisplayNames, Segmenter e Intl.Locale; currencySign: "accounting" e currencyDisplay: "name" formatam como standard e code.

#Desempenho

Todas essas classes são nativas: a análise de URLs, a codificação e decodificação de texto, a construção de corpos multipart e a movimentação de chunks de stream acontecem em Rust, não no QuickJS. Ainda assim, toda chamada do JS atravessa para o runtime, então prefira operações sobre o buffer inteiro (res.json(), res.text(), encoder.encode(s)) a loops byte a byte em JS. Veja Desempenho.