All files util.js

96.34% Statements 211/219
87.23% Branches 82/94
100% Functions 25/25
96.34% Lines 211/219

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 22017x 17x 17x 17x 17x 17x 17x 17x 676x 676x 17x 17x 17x 11x 11x 17x 17x 17x 17x 17x 17x 12x 17x 17x 17x 17x 17x 1741x 1x 1x 1x 1x 1741x 17x 17x 17x 87x 85x 87x 87x 87x 17x 17x 17x 17x 17x 17x 8x 2x 2x 2x 2x 2x 2x 2x 2x 2x 1x 1x 8x 17x 17x 17x 17x 17x 17x 9795x 9795x 662x 9x 9795x 17x 17x 17x 94x 94x 94x 94x       94x 17x 17x 17x 72x 72x 72x 72x 72x 513x 513x 513x 72x 72x 17x 17x 17x 142x 142x 142x 142x 142x 4175839x 4175839x 1x 1x 141x 142x 17x 17x 17x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 1x 1x 1x 193x 17x 17x 17x 363x 363x 363x 363x 17x 17x 17x 1366x 1343x 1366x 1366x 17x 17x 17x 363x 363x 363x 363x 363x 363x     363x 17x 17x 17x 149x 149x 149x 149x 17x 17x 17x 442x 230x 442x 442x 17x 17x 17x 203x 203x 17x 17x 17x 187x 187x 187x 186x 187x 17x 17x 17x 17x 17x 17x 34x 34x 34x 35x       35x 34x 34x 34x 34x 34x 34x 34x 35x 2x 2x 35x 35x 35x 35x 34x 32x 32x 32x 34x 34x 34x 17x 17x 17x 1107x 1107x  
// Copyright 2026 优谷雅言 open.shengzhiai.com. Licensed under the Apache License, Version 2.0.
// Small helpers shared by the SDK modules. Browser and Node 18+ compatible, no dependencies.
 
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder('utf-8');
 
/** UTF-8 encode a string. */
export function utf8(str) {
  return textEncoder.encode(String(str));
}
 
/** UTF-8 decode bytes. */
export function utf8Decode(bytes) {
  return textDecoder.decode(bytes);
}
 
const toStringTag = (x) => Object.prototype.toString.call(x);
 
/** True for ArrayBuffer and SharedArrayBuffer, also across realms (iframes, vm contexts). */
export function isArrayBuffer(x) {
  if (x instanceof ArrayBuffer) return true;
  const tag = toStringTag(x);
  return tag === '[object ArrayBuffer]' || tag === '[object SharedArrayBuffer]';
}
 
/** The global object in browsers, workers and Node, also where globalThis is missing. */
export function getGlobal() {
  if (typeof globalThis !== 'undefined') return globalThis;
  /* c8 ignore next 3 */
  if (typeof self !== 'undefined') return self;
  if (typeof window !== 'undefined') return window;
  return {};
}
 
/** True for Blob and File (anything with size plus arrayBuffer(), or an instance of Blob). */
export function isBlobLike(x) {
  if (x == null || typeof x !== 'object') return false;
  const g = getGlobal();
  if (typeof g.Blob === 'function' && x instanceof g.Blob) return true;
  return typeof x.arrayBuffer === 'function' && typeof x.size === 'number';
}
 
/**
 * Bytes of a Blob. Blob.arrayBuffer() exists from Chrome 76, Firefox 69 and Safari 14; older
 * browsers read through FileReader.
 */
export function readBlob(blob) {
  if (typeof blob.arrayBuffer === 'function') return blob.arrayBuffer();
  const g = getGlobal();
  if (typeof g.FileReader === 'function') {
    return new Promise((resolve, reject) => {
      const fr = new g.FileReader();
      fr.onload = () => resolve(fr.result);
      fr.onerror = () => reject(fr.error);
      fr.readAsArrayBuffer(blob);
    });
  }
  /* c8 ignore next */
  return new g.Response(blob).arrayBuffer();
}
 
/**
 * View ArrayBuffer or any ArrayBufferView as Uint8Array without copying.
 * Returns null for other inputs.
 */
export function toBytes(data) {
  if (data instanceof Uint8Array) return data;
  if (isArrayBuffer(data)) return new Uint8Array(data);
  if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
  return null;
}
 
/** Copy of the bytes in a fresh ArrayBuffer of exactly that length. */
export function toExactArrayBuffer(bytes) {
  const buf = bytes.buffer;
  if (bytes.byteOffset === 0 && bytes.byteLength === buf.byteLength && (buf instanceof ArrayBuffer || toStringTag(buf) === '[object ArrayBuffer]')) {
    return buf;
  }
  const copy = new Uint8Array(bytes.byteLength);
  copy.set(bytes);
  return copy.buffer;
}
 
/** Concatenate byte chunks. */
export function concatBytes(chunks) {
  let total = 0;
  for (const c of chunks) total += c.length;
  const out = new Uint8Array(total);
  let off = 0;
  for (const c of chunks) {
    out.set(c, off);
    off += c.length;
  }
  return out;
}
 
/** Index of needle in haystack (bytes), or -1. */
export function indexOfBytes(haystack, needle, from = 0) {
  const n = needle.length;
  if (n === 0) return from;
  const first = needle[0];
  const last = haystack.length - n;
  outer: for (let i = from; i <= last; i++) {
    if (haystack[i] !== first) continue;
    for (let j = 1; j < n; j++) if (haystack[i + j] !== needle[j]) continue outer;
    return i;
  }
  return -1;
}
 
/** Standard base64 (with padding) of ArrayBuffer or ArrayBufferView. */
export function bytesToBase64(input) {
  const bytes = toBytes(input);
  if (!bytes) throw new TypeError('bytesToBase64 expects an ArrayBuffer or ArrayBufferView');
  if (typeof btoa === 'function') {
    let bin = '';
    const CHUNK = 0x8000;
    for (let i = 0; i < bytes.length; i += CHUNK) {
      bin += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK));
    }
    return btoa(bin);
  }
  /* c8 ignore next 2 */
  // eslint-disable-next-line no-undef
  return Buffer.from(bytes).toString('base64');
}
 
/** Lowercase hex of bytes. */
export function toHex(bytes) {
  let s = '';
  for (let i = 0; i < bytes.length; i++) s += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);
  return s;
}
 
/** The Web Crypto object to use: injected first, then globalThis.crypto. */
export function getCrypto(injected) {
  if (injected) return injected;
  const g = getGlobal();
  return g.crypto || null;
}
 
/** n random bytes from crypto.getRandomValues, Math.random only when no crypto exists. */
export function randomBytes(n, cryptoImpl) {
  const out = new Uint8Array(n);
  const c = getCrypto(cryptoImpl);
  if (c && typeof c.getRandomValues === 'function') {
    c.getRandomValues(out);
    return out;
  }
  for (let i = 0; i < n; i++) out[i] = Math.floor(Math.random() * 256);
  return out;
}
 
/** Truncate a string to max UTF-16 units. */
export function truncate(s, max) {
  if (s == null) return s;
  const str = String(s);
  return str.length > max ? str.slice(0, max) : str;
}
 
/** Plain object check. */
export function isPlainObject(x) {
  if (x === null || typeof x !== 'object') return false;
  const p = Object.getPrototypeOf(x);
  return p === Object.prototype || p === null;
}
 
/** Remove trailing slashes. */
export function stripTrailingSlash(url) {
  return String(url).replace(/\/+$/, '');
}
 
/** Detect a browser or web worker (where fetch cannot set User-Agent). */
export function isBrowserLike() {
  const g = getGlobal();
  if (typeof g.window !== 'undefined' && typeof g.document !== 'undefined') return true;
  if (typeof g.importScripts === 'function') return true;
  return false;
}
 
/**
 * Promise that resolves after ms, or rejects with the value returned by onAbort when any of the
 * given AbortSignal-like objects fires.
 */
export function abortableDelay(ms, signals, onAbort) {
  return new Promise((resolve, reject) => {
    const list = (signals || []).filter(Boolean);
    for (const s of list) {
      if (s.aborted) {
        reject(onAbort(s));
        return;
      }
    }
    let timer = null;
    const cleanups = [];
    const done = () => {
      if (timer !== null) clearTimeout(timer);
      for (const c of cleanups) c();
    };
    for (const s of list) {
      const h = () => {
        done();
        reject(onAbort(s));
      };
      s.addEventListener('abort', h, { once: true });
      cleanups.push(() => s.removeEventListener('abort', h));
    }
    timer = setTimeout(() => {
      timer = null;
      done();
      resolve();
    }, Math.max(0, ms));
  });
}
 
/** Number check that also rejects NaN and Infinity. */
export function isFiniteNumber(x) {
  return typeof x === 'number' && Number.isFinite(x);
}