tricu

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

local-modules-ui.js (5343B)


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