@daugasauron/dolly-host-save 0.0.0-stage → 0.1.4-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,4 @@
1
- # Temporary Holding Version
1
+ # @daugasauron/dolly-host-save
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Dolly's `download@0` host module: offer a file to save; the user clicks. Give the URL of its
4
+ `module.json` to `boot()`'s `modules`; @daugasauron/dolly's README says how.
package/abi.mjs ADDED
@@ -0,0 +1,10 @@
1
+ // Generated from host/download/dolly-download-0.wat, host/download/download.h, include/dolly/process.h.
2
+ export const DOLLY_DOWNLOAD_OPEN = 1;
3
+ export const DOLLY_DOWNLOAD_WRITE = 2;
4
+ export const DOLLY_DOWNLOAD_CLOSE = 3;
5
+ export const DOLLY_DOWNLOAD_ABORT = 4;
6
+ export const DOLLY_DOWNLOAD_CHUNK_CAPACITY = 1048576;
7
+ export const DOLLY_DOWNLOAD_MAX_SIZE = 1073741824;
8
+ export const DOLLY_DOWNLOAD_FILE = 51;
9
+ export const DOLLY_ERRNO = Object.freeze({ E2BIG: 1, EACCES: 2, EADDRINUSE: 3, EADDRNOTAVAIL: 4, EAFNOSUPPORT: 5, EAGAIN: 6, EALREADY: 7, EBADF: 8, EBADMSG: 9, EBUSY: 10, ECANCELED: 11, ECHILD: 12, ECONNABORTED: 13, ECONNREFUSED: 14, ECONNRESET: 15, EDEADLK: 16, EDESTADDRREQ: 17, EDOM: 18, EDQUOT: 19, EEXIST: 20, EFAULT: 21, EFBIG: 22, EHOSTUNREACH: 23, EIDRM: 24, EILSEQ: 25, EINPROGRESS: 26, EINTR: 27, EINVAL: 28, EIO: 29, EISCONN: 30, EISDIR: 31, ELOOP: 32, EMFILE: 33, EMLINK: 34, EMSGSIZE: 35, EMULTIHOP: 36, ENAMETOOLONG: 37, ENETDOWN: 38, ENETRESET: 39, ENETUNREACH: 40, ENFILE: 41, ENOBUFS: 42, ENODEV: 43, ENOENT: 44, ENOEXEC: 45, ENOLCK: 46, ENOLINK: 47, ENOMEM: 48, ENOMSG: 49, ENOPROTOOPT: 50, ENOSPC: 51, ENOSYS: 52, ENOTCONN: 53, ENOTDIR: 54, ENOTEMPTY: 55, ENOTRECOVERABLE: 56, ENOTSOCK: 57, ENOTTY: 59, ENXIO: 60, EOVERFLOW: 61, EOWNERDEAD: 62, EPERM: 63, EPIPE: 64, EPROTO: 65, EPROTONOSUPPORT: 66, EPROTOTYPE: 67, ERANGE: 68, EROFS: 69, ESPIPE: 70, ESRCH: 71, ESTALE: 72, ETIMEDOUT: 73, ETXTBSY: 74, EXDEV: 75, ENODATA: 116, ENOTSUP: 138 });
10
+ export const DOLLY_DOWNLOAD_ABI_DIGEST = "f5c6e4ad11e3859791578d73cce40946eb3528da93dc6971c17951faac642b4f";
package/download.mjs ADDED
@@ -0,0 +1,158 @@
1
+ import { DOLLY_ERRNO as E } from "./abi.mjs";
2
+ import { DOLLY_DOWNLOAD_OPEN, DOLLY_DOWNLOAD_WRITE, DOLLY_DOWNLOAD_CLOSE, DOLLY_DOWNLOAD_ABORT,
3
+ DOLLY_DOWNLOAD_CHUNK_CAPACITY, DOLLY_DOWNLOAD_MAX_SIZE } from "./abi.mjs";
4
+ export { DOLLY_DOWNLOAD_ABI_DIGEST as digest } from "./abi.mjs";
5
+
6
+ // Offers awaiting the user's Save/Dismiss click, the open stream included; more fail with EBUSY.
7
+ const maxPending = 4;
8
+ const maxRetainedUrls = 4;
9
+ // No separators, controls or bidi controls that could disguise the saved name.
10
+ function validName(name) {
11
+ return typeof name === "string" && name.length > 0 && name.length <= 255 &&
12
+ name !== "." && name !== ".." &&
13
+ !/[\/\\\u0000-\u001f\u007f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/u.test(name);
14
+ }
15
+ const sizeLabel = bytes => bytes < 1024 * 1024 ? `${Math.ceil(bytes / 1024)} KiB`
16
+ : `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
17
+
18
+ // Wasm can only ask. Each file reaches the browser's download manager through
19
+ // one user click on its Save button, never automatically.
20
+ export function browser({ keyboard, indicators }) {
21
+ const pending = new Int32Array(new SharedArrayBuffer(4));
22
+ const panel = document.createElement("ul");
23
+ panel.id = "downloads";
24
+ panel.className = "page-indicator";
25
+ panel.setAttribute("aria-label", "Files Dolly offers to save");
26
+ panel.hidden = true;
27
+ document.body.append(panel);
28
+ const look = document.head.appendChild(Object.assign(document.createElement("style"), { textContent: `
29
+ #downloads { position: fixed; left: 0.6rem; bottom: 0.6rem; z-index: 7; margin: 0; padding: 0.5rem; list-style: none;
30
+ max-width: calc(100vw - 1.2rem); color: #e8e3d7; background: #262626; border: 1px solid #f2d45c; font-size: 14px; }
31
+ #downloads[hidden] { display: none; } #downloads li + li { margin-top: 0.4rem; }
32
+ #downloads button { margin-right: 0.4rem; color: inherit; background: #333; border: 1px solid #77736c;
33
+ font: inherit; cursor: pointer; }` }));
34
+ const urls = [];
35
+ // The item of the stream the Worker is still receiving.
36
+ let preparing = null;
37
+ function revoke(url) {
38
+ const index = urls.indexOf(url);
39
+ if (index >= 0) urls.splice(index, 1);
40
+ URL.revokeObjectURL(url);
41
+ }
42
+ function save(name, file) {
43
+ const url = URL.createObjectURL(file);
44
+ urls.push(url);
45
+ if (urls.length > maxRetainedUrls) revoke(urls[0]);
46
+ setTimeout(() => revoke(url), 60_000);
47
+ const link = document.createElement("a");
48
+ link.href = url; link.download = name;
49
+ link.click();
50
+ }
51
+ // An offer waiting for the user's click holds the page's indicators shown.
52
+ function offer(item) {
53
+ panel.append(item);
54
+ panel.hidden = false;
55
+ indicators.hold("download", true);
56
+ }
57
+ function remove(item) {
58
+ item.remove();
59
+ panel.hidden = panel.childElementCount === 0;
60
+ indicators.hold("download", !panel.hidden);
61
+ }
62
+ return {
63
+ configuration: { pending: pending.buffer },
64
+ messages: {
65
+ "download-progress"({ name, size }) {
66
+ if (!validName(name) || !Number.isSafeInteger(size)) throw new Error("Dolly supplied an invalid download");
67
+ if (!preparing) {
68
+ preparing = document.createElement("li");
69
+ preparing.dataset.name = name;
70
+ offer(preparing);
71
+ }
72
+ preparing.textContent = `Preparing ${name} (${sizeLabel(size)})`;
73
+ },
74
+ "download-abort"() {
75
+ if (preparing) remove(preparing);
76
+ preparing = null;
77
+ },
78
+ download({ name, file }) {
79
+ if (!validName(name) || !(file instanceof Blob) || file.size > DOLLY_DOWNLOAD_MAX_SIZE) {
80
+ throw new Error("Dolly supplied an invalid download request");
81
+ }
82
+ const item = preparing ?? document.createElement("li");
83
+ preparing = null;
84
+ item.dataset.name = name;
85
+ const button = (label, action) => {
86
+ const element = document.createElement("button");
87
+ element.type = "button";
88
+ element.textContent = label;
89
+ element.addEventListener("click", () => {
90
+ action();
91
+ remove(item);
92
+ Atomics.sub(pending, 0, 1);
93
+ keyboard?.focus({ preventScroll: true });
94
+ }, { once: true });
95
+ return element;
96
+ };
97
+ item.replaceChildren(button(`Save ${name} (${sizeLabel(file.size)})`, () => save(name, file)),
98
+ button("Dismiss", () => {}));
99
+ offer(item);
100
+ document.documentElement.dataset.downloadName = name;
101
+ },
102
+ },
103
+ claimsKey: event => panel.contains(event.target),
104
+ dispose() { panel.remove(); look.remove(); for (const url of [...urls]) revoke(url); },
105
+ };
106
+ }
107
+
108
+ // Each chunk becomes a Blob part at once through one staging buffer (a Blob
109
+ // copies its parts), so the Worker allocates nothing per chunk; the browser's
110
+ // Blob storage holds the file until it is saved.
111
+ export function worker({ send, get, configuration }) {
112
+ const decode = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
113
+ const pending = new Int32Array(configuration.pending);
114
+ const staging = new Uint8Array(DOLLY_DOWNLOAD_CHUNK_CAPACITY);
115
+ let stream = null;
116
+ return { bindings: { "env.dolly_download_dispatch": (operation, address, length) => {
117
+ const memory = get("runtime").memory.buffer;
118
+ const [start, size] = [address, length].map(Number);
119
+ if (![start, size].every(Number.isSafeInteger) || start < 0 || size < 0 ||
120
+ start > memory.byteLength - size) return -E.EINVAL;
121
+ const span = new Uint8Array(memory, start, size);
122
+ if (operation === DOLLY_DOWNLOAD_OPEN) {
123
+ if (size < 1 || size > 255) return -E.EINVAL;
124
+ let name;
125
+ try { name = decode.decode(span.slice()); }
126
+ catch { return -E.EINVAL; }
127
+ if (!validName(name)) return -E.EINVAL;
128
+ if (stream) return -E.EBUSY;
129
+ if (Atomics.add(pending, 0, 1) >= maxPending) {
130
+ Atomics.sub(pending, 0, 1);
131
+ return -E.EBUSY;
132
+ }
133
+ stream = { name, parts: [], size: 0 };
134
+ send({ type: "download-progress", name, size: 0 });
135
+ return 0;
136
+ }
137
+ if (!stream) return -E.EBADF;
138
+ if (operation === DOLLY_DOWNLOAD_WRITE) {
139
+ if (size < 1 || size > DOLLY_DOWNLOAD_CHUNK_CAPACITY) return -E.EINVAL;
140
+ if (size > DOLLY_DOWNLOAD_MAX_SIZE - stream.size) return -E.EFBIG;
141
+ staging.set(span);
142
+ stream.parts.push(new Blob([staging.subarray(0, size)]));
143
+ stream.size += size;
144
+ send({ type: "download-progress", name: stream.name, size: stream.size });
145
+ return 0;
146
+ }
147
+ if (size !== 0 || ![DOLLY_DOWNLOAD_CLOSE, DOLLY_DOWNLOAD_ABORT].includes(operation)) return -E.EINVAL;
148
+ if (operation === DOLLY_DOWNLOAD_CLOSE) {
149
+ send({ type: "download", name: stream.name,
150
+ file: new Blob(stream.parts, { type: "application/octet-stream" }) });
151
+ } else {
152
+ Atomics.sub(pending, 0, 1);
153
+ send({ type: "download-abort" });
154
+ }
155
+ stream = null;
156
+ return 0;
157
+ } } };
158
+ }
package/module.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "download",
3
+ "version": 0,
4
+ "gives": "offer a file to save; the user clicks",
5
+ "dependencies": ["runtime@0"],
6
+ "imports": ["env.dolly_download_dispatch"],
7
+ "host": "download.mjs",
8
+ "contracts": ["dolly-download-0.wat"],
9
+ "process": [],
10
+ "headers": ["download.h", "download-abi.h"],
11
+ "kernel": ["kernel.c"],
12
+ "client": ["client.c"]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
+ "version": "0.1.4-rc.2",
3
+ "type": "module",
2
4
  "name": "@daugasauron/dolly-host-save",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
5
+ "license": "MIT",
6
+ "dependencies": {
7
+ "@daugasauron/dolly": "0.1.4-rc.2"
8
+ },
9
+ "description": "Dolly's download@0 host module: offer a file to save; the user clicks.",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/daugasauron/dolly.git"
13
+ }
14
+ }