tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

local-modules-ui.ts (4988B)


      1 import { BUNDLE_LIMIT } from './bundle.js';
      2 import { LOCAL_MODULE_BYTES, LOCAL_MODULE_COUNT, type LocalModule, type LocalModuleInput } from './local-modules.js';
      3 // Hold local module files in page memory until removal or page departure.
      4 export function localModulesUI(changed: () => void): { inputs(): LocalModuleInput[]; entries(): { label: string; manifest: string; names: string[] }[]; disable(busy: boolean): void } {
      5   const input = document.getElementById('local-module-file') as HTMLInputElement;
      6   const list = document.getElementById('local-module-list')!;
      7   const status = document.getElementById('local-module-status')!;
      8   const cancel = document.getElementById('cancel-local-module') as HTMLButtonElement;
      9   const modules = new Map<string, { label: string; module: LocalModule }>();
     10   let busy = false, checking = false, abort: (() => void) | undefined;
     11   const render = (): void => {
     12     input.disabled = busy || checking; cancel.hidden = !checking;
     13     list.replaceChildren();
     14     for (const [manifest, { label, module }] of modules) {
     15       const row = document.createElement('li'), title = document.createElement('p');
     16       title.textContent = `${label} · ${module.names.length} export(s) · ${module.bytes.byteLength.toLocaleString()} bytes`;
     17       const code = document.createElement('code'); code.textContent = `!import ${manifest} as Local`;
     18       const details = document.createElement('details'), summary = document.createElement('summary'), names = document.createElement('p');
     19       summary.textContent = 'Exports and bundle hash'; names.textContent = `${module.names.join(', ')}\nBundle SHA-256: ${module.hash}`; details.append(summary, names);
     20       const copy = document.createElement('button'); copy.textContent = 'Copy import';
     21       copy.onclick = () => { void (async () => { try { await navigator.clipboard.writeText(code.textContent!); status.textContent = 'Import copied.'; } catch { status.textContent = code.textContent; } })(); };
     22       const remove = document.createElement('button'); remove.textContent = 'Remove'; remove.dataset['removeModule'] = manifest; remove.disabled = busy || checking;
     23       remove.onclick = () => { modules.delete(manifest); render(); status.textContent = 'Local module removed. Draft imports will be resolved again.'; changed(); };
     24       row.append(title, code, details, copy, remove); list.append(row);
     25     }
     26   };
     27   cancel.onclick = () => abort?.();
     28   input.onchange = () => {
     29     const file = input.files?.[0]; input.value = ''; if (!file || busy || checking) return;
     30     if (!file.size || file.size > BUNDLE_LIMIT) { status.textContent = 'Choose a nonempty .arboricx file of at most 16 MiB.'; return; }
     31     checking = true; render(); status.textContent = 'Checking module locally…';
     32     void (async () => {
     33       let worker: Worker | undefined, active = true;
     34       let timer: ReturnType<typeof setTimeout> | undefined;
     35       try {
     36         const current = new Worker(new URL('./local-module-worker.js', import.meta.url), { type: 'module' }); worker = current;
     37         const module = await new Promise<LocalModule>((resolve, reject) => {
     38           abort = () => reject(new Error('Local file check cancelled.'));
     39           timer = setTimeout(() => reject(new Error('Local file check timed out.')), 60_000);
     40           current.onmessage = ({ data }: MessageEvent<{ module?: LocalModule; error?: string }>) => data.module ? resolve(data.module) : reject(new Error(data.error ?? 'Invalid local module'));
     41           current.onerror = event => { event.preventDefault(); reject(new Error(event.message || 'Local file check failed')); };
     42           void file.arrayBuffer().then(bytes => { if (active) current.postMessage(new Uint8Array(bytes)); }).catch(reject);
     43         });
     44         if (modules.has(module.manifest)) { status.textContent = 'This module manifest is already available locally.'; return; }
     45         const total = [...modules.values()].reduce((n, item) => n + item.module.bytes.byteLength, module.bytes.byteLength);
     46         if (modules.size >= LOCAL_MODULE_COUNT || total > LOCAL_MODULE_BYTES) throw new Error('Local collection limit: 32 modules / 64 MiB. Remove a module first.');
     47         modules.set(module.manifest, { label: file.name.slice(0, 256), module });
     48         status.textContent = 'Available locally by manifest hash.';
     49         changed();
     50       } catch (error) { status.textContent = String(error); }
     51       finally { active = false; clearTimeout(timer); worker?.terminate(); abort = undefined; checking = false; render(); }
     52     })();
     53   };
     54   render();
     55   return { entries: () => [...modules.values()].map(({ label, module }) => ({ label, manifest: module.manifest, names: [...module.names] })), inputs: () => [...modules.values()].map(({ module }) => ({ manifest: module.manifest, bytes: module.bytes })), disable: value => { busy = value; input.disabled = busy || checking; for (const button of list.querySelectorAll<HTMLButtonElement>('[data-remove-module]')) button.disabled = busy || checking; } };
     56 }