UNPKG

manifold-3d

Version:

Geometry library for topological robustness

198 lines 7.39 kB
// Copyright 2025 The Manifold Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @group ManifoldCAD * @packageDocumentation */ import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'; import convert from 'convert-source-map'; import { FetchError } from "./error.js"; /** * Are we in a web worker? * * @returns A boolean. */ export const isWebWorker = () => typeof self !== 'undefined' && typeof self.document == 'undefined'; /** * Are we in Node? * * @returns A boolean. */ export const isNode = () => typeof process !== 'undefined' && !!process?.versions?.node; const parseV8StackTrace = (stack) => stack.split('\n').filter(frame => frame.match(/<anonymous>/)).map(frame => { const matches = frame.matchAll(/:([0-9]+):([0-9]+)\)$/g).next().value; const [line, column] = [parseInt(matches[1]), parseInt(matches[2])]; const methodName = frame.match(/^\s+at\s([^\s]+)/)[1]; return { line, column, // In Node or Chrome, a function constructor shows as 'eval'. methodName: methodName === 'eval' ? null : methodName }; }); const parseSpiderMonkeyStackTrace = (stack) => stack.split('\n') .filter(frame => frame.match(/AsyncFunction/)) .map(frame => { const matches = frame.matchAll(/AsyncFunction:([0-9]+):([0-9]+)/g).next().value; const [line, column] = [parseInt(matches[1]), parseInt(matches[2])]; const methodName = frame.match(/^[^@]+/)[0]; return { line, column, // In Firefox, a function constructor shows as 'anonymous'. methodName: methodName === 'anonymous' ? null : methodName }; }); /** * Attempt to parse a stack trace from a dynamically created function. * * This makes the stack trace more readable at the potential cost of confusion * if a manifoldCAD user is also constructing new Functions within their model. * * @internal */ export const parseStackTrace = (stack) => { if (stack.match(/^\s+at\s/gm)) { // V8 -- Chrome, NodeJS return parseV8StackTrace(stack); } else if (stack.match(/^([^@]+)@/gm)) { // SpiderMonkey -- Firefox return parseSpiderMonkeyStackTrace(stack); } else return []; }; export const getSourceMappedStackTrace = (code, error, lineOffset = 0) => { const converter = convert.fromSource(code); if (!converter || !error.stack) { // No inline source map. We can't do anything. return error.stack; } const parsed = parseStackTrace(error.stack); if (!parsed.length) { // We can't parse this. Chances are, it's someone in Safari. return error.stack; } const tracer = new TraceMap(converter.toObject()); const stack = parsed.map(frame => { if ((frame.line + lineOffset) < 1) { return frame; // Line number is out of range. } const { line, column, source: file } = originalPositionFor(tracer, { line: frame.line + lineOffset, column: frame.column }); const { methodName } = frame; // column numbers should be 1 indexed. Results are 0 indexed. return { line, column: column + 1, file, methodName }; }); return [ error.toString(), ...stack.map((frame) => { const location = `${frame.file}:${frame.line}:${frame.column}`; if (frame.methodName) { return ` at ${frame.methodName} (${location})`; } else { return ` at ${location}`; } }) ].reduce((acc, cur) => `${acc}\n${cur}`); }; export const findExtension = (extension, list) => { let match = list.find(entry => entry.extension === extension); if (match) return match; match = list.find(entry => `.${entry.extension}` === extension); if (match) return match; // Do we have a filename instead of just an extension? const fileExt = extension.match(/(\.[^\.]+)$/); if (!fileExt) return null; const [ext] = fileExt; match = list.find(entry => entry.extension === ext); if (match) return match; match = list.find(entry => `.${entry.extension}` === ext); return match; }; export const findMimeType = (mimetype, list) => list.find(entry => entry.mimetype === mimetype); export function formatLength(mm) { if (Math.abs(mm) >= 10000) { return `${(mm / 1000).toLocaleString(undefined, { maximumFractionDigits: 2 })} m`; } return `${mm.toLocaleString(undefined, { maximumFractionDigits: 2 })} mm`; } export function formatArea(mm2) { const cm2 = mm2 / 100; if (Math.abs(cm2) >= 10000) { return `${(cm2 / 10000).toLocaleString(undefined, { maximumFractionDigits: 2 })} m^2`; } return `${cm2.toLocaleString(undefined, { maximumFractionDigits: 2 })} cm^2`; } export function formatVolume(mm3) { const cm3 = mm3 / 1000; if (Math.abs(cm3) >= 1_000_000) { return `${(cm3 / 1_000_000).toLocaleString(undefined, { maximumFractionDigits: 2 })} m^3`; } return `${cm3.toLocaleString(undefined, { maximumFractionDigits: 2 })} cm^3`; } const FETCH_ATTEMPTS = 3; const FETCH_BASE_DELAY_MS = 200; const FETCH_FACTOR = 3; const isRetryableStatus = (status) => status >= 500 || status === 429; const inputToUrl = (input) => { if (typeof input === 'string') return input; if (input instanceof URL) return input.toString(); return input.url; }; const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); /** * Wraps `fetch` with retry-on-transient-failure and throws `FetchError` on * non-2xx. Retries on HTTP 5xx, HTTP 429, and `TypeError` (network errors * like "Failed to fetch"); returns immediately on other 4xx so typo'd URLs * fail fast. AbortError and other exceptions propagate untouched. * * Exponential backoff: 200ms, 600ms; ~800ms worst-case added latency. */ export async function fetchWithRetry(input, init) { let response; for (let attempt = 0;; attempt++) { const isLast = attempt === FETCH_ATTEMPTS - 1; try { response = await fetch(input, init); if (!isRetryableStatus(response.status) || isLast) break; } catch (err) { // TypeError covers "Failed to fetch" / DNS / connection-reset // errors. Retry those unless out of attempts. AbortError and // NotAllowedError propagate untouched. if (!(err instanceof TypeError) || isLast) throw err; } await delay(FETCH_BASE_DELAY_MS * FETCH_FACTOR ** attempt); } // response is assigned on every path that reaches break. const r = response; if (!r.ok) { throw new FetchError(r.status, r.statusText, inputToUrl(input), await r.text()); } return r; } //# sourceMappingURL=util.js.map