| |
| |
| |
|
|
| import { defaultInitialFetchSize } from './metadata.js' |
|
|
| |
| |
| |
| |
| |
| |
| export function toJson(obj) { |
| if (obj === undefined) return null |
| if (typeof obj === 'bigint') return Number(obj) |
| if (Object.is(obj, -0)) return 0 |
| if (Array.isArray(obj)) return obj.map(toJson) |
| if (obj instanceof Uint8Array) return Array.from(obj) |
| if (obj instanceof Date) return obj.toISOString() |
| if (obj instanceof Object) { |
| |
| const newObj = {} |
| for (const key of Object.keys(obj)) { |
| if (obj[key] === undefined) continue |
| newObj[key] = toJson(obj[key]) |
| } |
| return newObj |
| } |
| return obj |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function concat(aaa, bbb) { |
| const chunk = 10000 |
| for (let i = 0; i < bbb.length; i += chunk) { |
| aaa.push(...bbb.slice(i, i + chunk)) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function equals(a, b, strict = true) { |
| |
| if (strict ? a === b : a == b) return true |
| if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false |
|
|
| if (a instanceof Uint8Array && b instanceof Uint8Array) { |
| if (a.length !== b.length) return false |
| for (let i = 0; i < a.length; i++) { |
| if (a[i] !== b[i]) return false |
| } |
| return true |
| } |
| if (Array.isArray(a) && Array.isArray(b)) { |
| if (a.length !== b.length) return false |
| for (let i = 0; i < a.length; i++) { |
| if (!equals(a[i], b[i], strict)) return false |
| } |
| return true |
| } |
|
|
| const aKeys = Object.keys(a) |
| if (aKeys.length !== Object.keys(b).length) return false |
| for (const k of aKeys) { |
| if (!equals(a[k], b[k], strict)) return false |
| } |
| return true |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| async function byteLengthFromUrlUsingGet(url, requestInit = {}, fetchFn = globalThis.fetch) { |
| const controller = new AbortController() |
| const headers = new Headers(requestInit.headers) |
| headers.set('Range', 'bytes=0-0') |
|
|
| const res = await fetchFn(url, { |
| ...requestInit, |
| headers, |
| signal: controller.signal, |
| }) |
|
|
| if (!res.ok) throw new Error(`fetch with range failed ${res.status}`) |
|
|
| |
| if (res.status === 206) { |
| const contentRange = res.headers.get('Content-Range') |
| if (!contentRange) throw new Error('missing content-range header') |
|
|
| |
| const match = contentRange.match(/bytes \d+-\d+\/(\d+)/) |
| if (!match) throw new Error(`invalid content-range header: ${contentRange}`) |
|
|
| return parseInt(match[1]) |
| } |
|
|
| |
| if (res.status === 200) { |
| const contentLength = res.headers.get('Content-Length') |
|
|
| |
| controller.abort() |
|
|
| if (contentLength) return parseInt(contentLength) |
| } |
|
|
| throw new Error('server does not support range requests and missing content-length') |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function byteLengthFromUrl(url, requestInit, customFetch) { |
| const fetch = customFetch ?? globalThis.fetch |
| const res = await fetch(url, { ...requestInit, method: 'HEAD' }) |
|
|
| |
| if (res.status === 403) { |
| return byteLengthFromUrlUsingGet(url, requestInit, fetch) |
| } |
|
|
| if (!res.ok) throw new Error(`fetch head failed ${res.status}`) |
| const length = res.headers.get('Content-Length') |
| |
| if (!length) { |
| return byteLengthFromUrlUsingGet(url, requestInit, fetch) |
| } |
| return parseInt(length) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function asyncBufferFromUrl({ url, byteLength, requestInit, fetch: customFetch }) { |
| if (!url) throw new Error('missing url') |
| const fetch = customFetch ?? globalThis.fetch |
| |
| byteLength ??= await byteLengthFromUrl(url, requestInit, fetch) |
|
|
| |
| |
| |
| |
| let buffer = undefined |
| const init = requestInit || {} |
|
|
| return { |
| byteLength, |
| async slice(start, end) { |
| if (buffer) { |
| return buffer.then(buffer => buffer.slice(start, end)) |
| } |
|
|
| const headers = new Headers(init.headers) |
| const endStr = end === undefined ? '' : end - 1 |
| headers.set('Range', `bytes=${start}-${endStr}`) |
|
|
| const res = await fetch(url, { ...init, headers }) |
| if (!res.ok || !res.body) throw new Error(`fetch failed ${res.status}`) |
|
|
| if (res.status === 200) { |
| |
| buffer = res.arrayBuffer() |
| return buffer.then(buffer => buffer.slice(start, end)) |
| } else if (res.status === 206) { |
| |
| return res.arrayBuffer() |
| } else { |
| throw new Error(`fetch received unexpected status code ${res.status}`) |
| } |
| }, |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function cachedAsyncBuffer({ byteLength, slice }, { minSize = defaultInitialFetchSize } = {}) { |
| if (byteLength < minSize) { |
| |
| const buffer = slice(0, byteLength) |
| return { |
| byteLength, |
| async slice(start, end) { |
| return (await buffer).slice(start, end) |
| }, |
| } |
| } |
| const cache = new Map() |
| return { |
| byteLength, |
| |
| |
| |
| |
| |
| slice(start, end) { |
| const key = cacheKey(start, end, byteLength) |
| const cached = cache.get(key) |
| if (cached) return cached |
| |
| const promise = slice(start, end) |
| cache.set(key, promise) |
| return promise |
| }, |
| } |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function cacheKey(start, end, size) { |
| if (start < 0) { |
| if (end !== undefined) throw new Error(`invalid suffix range [${start}, ${end}]`) |
| if (size === undefined) return `${start},` |
| return `${size + start},${size}` |
| } else if (end !== undefined) { |
| if (start > end) throw new Error(`invalid empty range [${start}, ${end}]`) |
| return `${start},${end}` |
| } else if (size === undefined) { |
| return `${start},` |
| } else { |
| return `${start},${size}` |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function flatten(chunks) { |
| if (!chunks) return [] |
| if (chunks.length === 1) return chunks[0] |
| |
| const output = [] |
| for (const chunk of chunks) { |
| concat(output, chunk) |
| } |
| return output |
| } |
|
|