1import { 2 WEBR_MODULE_URL, WEBR_VERSION, GCPLYR_VERSION, WEBR_PACKAGE_REPOSITORY, 3 GCPLYR_ARTIFACT_URL, GCPLYR_ARTIFACT_SHA256, GCPLYR_RUNTIME_DEPENDENCIES 4} from "./webr-gcplyr-config.js"; 5import { eligibleParameterMeasurements, parameterInputToCsv, gcplyrParameterScript, parseParameterCsv } from "./parameters-core.js"; 6 7let runtimePromise; 8 9function bytesToHex(bytes) { 10 return [...bytes].map(byte => byte.toString(16).padStart(2, "0")).join(""); 11} 12 13function tarText(bytes) { 14 const zero = bytes.indexOf(0); 15 return new TextDecoder().decode(zero === -1 ? bytes : bytes.subarray(0, zero)); 16} 17 18async function ensureDirectory(webR, path) { 19 const parts = path.split("/").filter(Boolean); 20 let current = ""; 21 for (const part of parts) { 22 current += "/" + part; 23 try { await webR.FS.mkdir(current); } catch { /* Directory already exists. */ } 24 } 25} 26 27async function extractTgzToLibrary(webR, artifact) { 28 const stream = new Blob([artifact]).stream().pipeThrough(new DecompressionStream("gzip")); 29 const tar = new Uint8Array(await new Response(stream).arrayBuffer()); 30 const libraryPath = await webR.evalRString(".libPaths()[1]"); 31 for (let offset = 0; offset + 512 <= tar.length;) { 32 const header = tar.subarray(offset, offset + 512); 33 if (header.every(byte => byte === 0)) break; 34 const name = tarText(header.subarray(0, 100)); 35 const prefix = tarText(header.subarray(345, 500)); 36 const relativePath = (prefix ? prefix + "/" : "") + name; 37 const size = Number.parseInt(tarText(header.subarray(124, 136)).trim() || "0", 8); 38 const type = String.fromCharCode(header[156] || 48); 39 if (!relativePath || relativePath.startsWith("/") || relativePath.split("/").includes("..")) { 40 throw new Error("Unsafe path in pinned gcplyr artifact."); 41 } 42 const target = libraryPath + "/" + relativePath.replace(/\/$/, ""); 43 if (type === "5") { 44 await ensureDirectory(webR, target); 45 } else if (type === "0" || type === "\0") { 46 await ensureDirectory(webR, target.slice(0, target.lastIndexOf("/"))); 47 await webR.FS.writeFile(target, tar.slice(offset + 512, offset + 512 + size)); 48 } 49 offset += 512 + Math.ceil(size / 512) * 512; 50 } 51} 52 53async function installPinnedGcplyr(webR) { 54 await webR.installPackages(GCPLYR_RUNTIME_DEPENDENCIES, { repos: WEBR_PACKAGE_REPOSITORY }); 55 const response = await fetch(GCPLYR_ARTIFACT_URL); 56 if (!response.ok) throw new Error(`Unable to download pinned gcplyr artifact (HTTP ${response.status}).`); 57 const artifact = new Uint8Array(await response.arrayBuffer()); 58 const digest = bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", artifact))); 59 if (digest !== GCPLYR_ARTIFACT_SHA256) { 60 throw new Error(`Pinned gcplyr artifact checksum mismatch: expected ${GCPLYR_ARTIFACT_SHA256}, received ${digest}.`); 61 } 62 await extractTgzToLibrary(webR, artifact); 63} 64 65async function createRuntime(onStage) { 66 onStage("runtime", "loading", `Downloading webR ${WEBR_VERSION}`); 67 const { WebR, ChannelType } = await import(WEBR_MODULE_URL); 68 const webR = new WebR({ interactive: false, channelType: ChannelType.PostMessage }); 69 await webR.init(); 70 onStage("runtime", "done", `webR ${WEBR_VERSION} · PostMessage`); 71 onStage("package", "loading", `Loading checksum-pinned gcplyr ${GCPLYR_VERSION} and dependencies`); 72 await installPinnedGcplyr(webR); 73 const actualVersion = await webR.evalRString('as.character(packageVersion("gcplyr"))'); 74 if (actualVersion !== GCPLYR_VERSION) { 75 await webR.close(); 76 throw new Error(`Pinned gcplyr version mismatch: expected ${GCPLYR_VERSION}, received ${actualVersion}`); 77 } 78 onStage("package", "done", `gcplyr ${actualVersion}`); 79 return webR; 80} 81 82async function getRuntime(onStage) { 83 if (!runtimePromise) runtimePromise = createRuntime(onStage).catch(error => { runtimePromise = null; throw error; }); 84 const runtime = await runtimePromise; 85 onStage("runtime", "done", `webR ${WEBR_VERSION} · PostMessage`); 86 onStage("package", "done", `gcplyr ${GCPLYR_VERSION}`); 87 return runtime; 88} 89 90export async function runGcplyrParameters(processedMeasurements, onStage = () => {}) { 91 const eligible = eligibleParameterMeasurements(processedMeasurements); 92 if (!eligible.length) throw new Error("No eligible sample or control measurements are available for parameter analysis."); 93 const webR = await getRuntime(onStage); 94 onStage("data", "loading", "Copying processed measurements into browser-side R memory"); 95 await webR.FS.writeFile("/tmp/processed_measurements.csv", new TextEncoder().encode(parameterInputToCsv(eligible)));
96 onStage("data", "done", `${eligible.length.toLocaleString()} rows · no upload`); 97 onStage("analysis", "loading", "Calculating replicate-level growth parameters"); 98 await webR.evalRVoid(gcplyrParameterScript()); 99 const bytes = await webR.FS.readFile("/tmp/gcplyr_parameter_results.csv"); 100 const rows = parseParameterCsv(new TextDecoder().decode(bytes)); 101 onStage("analysis", "done", `${rows.length} replicate curves analysed`); 102 return { rows, eligibleMeasurements: eligible.length, gcplyrVersion: GCPLYR_VERSION, webRVersion: WEBR_VERSION }; 103}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.