feat(improvise-2mqz): add wire forms for SaveAs and ExportCsv downloads
This commit implements the download flow for SaveAs and ExportCsv per the USER RULING on improvise-2mqz. The correct shape is: - A request frame asks the host for BYTES - ServerFrame returns the bytes (or Rejected) - Browser triggers downloads via Blob + <a download> Key changes: Protocol layer (improvise-protocol): - Add Request::DownloadDocument and Request::DownloadCsv variants - Add ClientFrame::DownloadDocument and ClientFrame::DownloadCsv - Add ServerFrame::DocumentBytes and ServerFrame::CsvBytes - Add KeyOutcome::LocalThenRequest variant for operations that need both local fold (mode change, buffer clear) AND a server request - Update stranding() to unstrand SavePrompt and ExportPrompt modes - Update keymap to use rebind() instead of commit() for commit-save and commit-export (because LocalThenRequest can't be in a Sequence) Host layer (improvise-command): - Handle new ClientFrames in host.rs returning bytes or Rejected - Add Rejection::ExportFailed variant for CSV export failures - Update CommitSave and CommitExport to clear their buffers - Make render_csv() public in improvise-io for host use WASM client layer (improvise-wasm-client): - Add PendingDownload type with filename, bytes, and kind - Store pending downloads for JavaScript retrieval via take_pending_download() Browser layer (web/app.js): - Add triggerDownload() function using Blob + <a download> - Poll for pending downloads after frame handling This unstrands SavePrompt and ExportPrompt which were previously refused by stranding() due to missing wire forms. Refs: improvise-2mqz Co-authored-by: Ed Langley <fiddlerwoaroof@users.noreply.github.com>
This commit is contained in:
+26
@@ -155,9 +155,35 @@ function onFrame(text) {
|
||||
console.error("improvise: frame rejected", text, err);
|
||||
return;
|
||||
}
|
||||
// Check for pending downloads (improvise-2mqz). The WASM client stores
|
||||
// download bytes from DocumentBytes/CsvBytes frames; we trigger the download
|
||||
// here via Blob + <a download>.
|
||||
try {
|
||||
const downloadJson = client.take_pending_download();
|
||||
if (downloadJson) {
|
||||
const download = JSON.parse(downloadJson);
|
||||
triggerDownload(download.filename, download.bytes, download.kind);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("improvise: download failed", err);
|
||||
}
|
||||
repaint();
|
||||
}
|
||||
|
||||
/** Trigger a browser download from bytes (improvise-2mqz). */
|
||||
function triggerDownload(filename, bytes, kind) {
|
||||
const mimeType = kind === "Document" ? "text/plain" : "text/csv";
|
||||
const blob = new Blob([new Uint8Array(bytes)], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// Transports.
|
||||
|
||||
/** The in-page worker: the standalone deployment, no server process. */
|
||||
|
||||
Reference in New Issue
Block a user