@pie-project/server-web 0.5.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/src/worker.mjs ADDED
@@ -0,0 +1,234 @@
1
+ import init, * as bindings from "../pkg/pie_browser.js";
2
+ import * as platform from "../pkg/platform.mjs";
3
+
4
+ globalThis.__pieHost = {
5
+ fetchRange(offset, len, request) {
6
+ queueMicrotask(() => bindings.pie_range_failed(request, "no artifact is being served lazily"));
7
+ },
8
+ };
9
+
10
+ let wasm = null;
11
+ let tick = null;
12
+ let cacheFill = null;
13
+ const CACHE_FILE_LIMIT = 1900 * 1048576;
14
+
15
+ async function fillCache(href, dir, key, size) {
16
+ const res = await fetch(href);
17
+ if (!res.ok) throw new Error(`${href}: ${res.status}`);
18
+ const handle = await dir.getFileHandle(key, { create: true });
19
+ const writable = await handle.createWritable();
20
+ const reader = res.body.getReader();
21
+ let got = 0;
22
+ try {
23
+ for (;;) {
24
+ const { done, value } = await reader.read();
25
+ if (done) break;
26
+ await writable.write(value);
27
+ got += value.length;
28
+ }
29
+ if (got !== size) throw new Error(`cache fill got ${got} of ${size} bytes`);
30
+ await writable.close();
31
+ console.log(`pie: artifact cached for the next visit (${(size / 1048576).toFixed(0)} MiB)`);
32
+ } catch (e) {
33
+ try { await writable.abort(); } catch { /* already gone */ }
34
+ try { await dir.removeEntry(key); } catch { /* nothing to remove */ }
35
+ throw e;
36
+ }
37
+ }
38
+ let tickScheduled = false;
39
+ let tickRunning = false;
40
+ let timer = null;
41
+
42
+ function scheduleTick(delayMs = 0) {
43
+ if (tickScheduled && (timer === null || delayMs > 0)) return;
44
+ tickScheduled = true;
45
+ if (timer) {
46
+ clearTimeout(timer);
47
+ timer = null;
48
+ }
49
+ const run = async () => {
50
+ tickScheduled = false;
51
+ if (tickRunning) {
52
+ tickScheduled = true;
53
+ return;
54
+ }
55
+ tickRunning = true;
56
+ let next;
57
+ try {
58
+ next = await tick();
59
+ } finally {
60
+ tickRunning = false;
61
+ }
62
+ if (tickScheduled) {
63
+ tickScheduled = false;
64
+ scheduleTick(0);
65
+ } else if (next >= 0) {
66
+ scheduleTick(next);
67
+ }
68
+ };
69
+ if (delayMs <= 0) queueMicrotask(run);
70
+ else timer = setTimeout(() => { timer = null; run(); }, delayMs);
71
+ }
72
+
73
+ let afterMessages = null;
74
+ function scheduleTickAfterMessages() {
75
+ if (afterMessages === null) {
76
+ const channel = new MessageChannel();
77
+ channel.port1.onmessage = () => {
78
+ afterMessages.armed = false;
79
+ scheduleTick(0);
80
+ };
81
+ afterMessages = { port: channel.port2, armed: false };
82
+ }
83
+ if (afterMessages.armed) return;
84
+ afterMessages.armed = true;
85
+ afterMessages.port.postMessage(null);
86
+ }
87
+
88
+ async function driven(promise) {
89
+ scheduleTick(0);
90
+ return await promise;
91
+ }
92
+
93
+ async function describe(url) {
94
+ const head = await fetch(url, { method: "HEAD" }).catch(() => null);
95
+ const size = Number(head?.headers.get("content-length") ?? 0);
96
+ const etag = head?.headers.get("etag") ?? "";
97
+ return { size, etag };
98
+ }
99
+
100
+ function cacheKey(name, size, etag) {
101
+ return `${name}|${size}|${etag}`.replace(/[^a-zA-Z0-9._-]/g, "_");
102
+ }
103
+
104
+ async function cacheDir() {
105
+ try {
106
+ return (await navigator.storage?.getDirectory?.()) ?? null;
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ export const host = {
113
+ async load(wasmUrl, logFilter) {
114
+ if (wasm) throw new Error("pie: the wasm is already loaded");
115
+ wasm = await init({ module_or_path: wasmUrl });
116
+ platform.attach(wasm, () => scheduleTick(0));
117
+ tick = WebAssembly.promising(wasm.pie_tick);
118
+ bindings.pie_init(logFilter);
119
+ return { worker: typeof WorkerGlobalScope !== "undefined" };
120
+ },
121
+
122
+ async bootLazy(url, config, base, onProgress) {
123
+ cacheFill = null;
124
+ const href = new URL(url, base ?? location.href).href;
125
+ const { size, etag } = await describe(href);
126
+ if (!size) throw new Error(`${url}: the server sent no content-length; a lazy boot needs the size up front`);
127
+ let file = null;
128
+ const dir = await cacheDir();
129
+ if (dir) {
130
+ try {
131
+ const cached = await (await dir.getFileHandle(cacheKey(url, size, etag))).getFile();
132
+ if (cached.size === size) file = cached;
133
+ } catch {
134
+ }
135
+ }
136
+ const from = file ? "cache" : "network";
137
+ let served = 0;
138
+ let requests = 0;
139
+ const range = async (offset, len) => {
140
+ requests += 1;
141
+ let bytes;
142
+ if (file) {
143
+ bytes = new Uint8Array(await file.slice(offset, offset + len).arrayBuffer());
144
+ } else {
145
+ const res = await fetch(href, { headers: { range: `bytes=${offset}-${offset + len - 1}` } });
146
+ if (res.status !== 206) {
147
+ throw new Error(`${url}: the server answered ${res.status} to a range request; a lazy boot needs ranges (206)`);
148
+ }
149
+ bytes = new Uint8Array(await res.arrayBuffer());
150
+ }
151
+ if (bytes.length !== len) throw new Error(`${url}: range ${offset}+${len} came back as ${bytes.length} bytes`);
152
+ served += len;
153
+ onProgress?.(served, size, from);
154
+ return bytes;
155
+ };
156
+ const refuse = globalThis.__pieHost.fetchRange;
157
+ globalThis.__pieHost.fetchRange = (offset, len, request) => {
158
+ range(offset, len).then(
159
+ (bytes) => bindings.pie_range_ready(request, bytes),
160
+ (e) => bindings.pie_range_failed(request, String(e?.message ?? e)),
161
+ );
162
+ };
163
+ const t0 = performance.now();
164
+ let summary;
165
+ try {
166
+ summary = JSON.parse(await driven(bindings.pie_boot_lazy(config, url.split("/").pop(), size)));
167
+ } finally {
168
+ globalThis.__pieHost.fetchRange = refuse;
169
+ console.log(`pie: lazy boot served ${requests} range request(s), ${(served / 1048576).toFixed(0)} MiB from ${from}, in ${(performance.now() - t0).toFixed(0)} ms`);
170
+ }
171
+ if (!file && dir && size <= CACHE_FILE_LIMIT) {
172
+ cacheFill = fillCache(href, dir, cacheKey(url, size, etag), size).catch((e) => {
173
+ console.warn("pie: background artifact cache fill failed:", e);
174
+ });
175
+ }
176
+ return summary;
177
+ },
178
+
179
+ async awaitCache() {
180
+ await cacheFill;
181
+ },
182
+
183
+ async installLanguage(language, wasmBytes) {
184
+ return await driven(bindings.pie_install_language(language, wasmBytes));
185
+ },
186
+ async install(wasmBytes, manifestToml) {
187
+ return await driven(bindings.pie_install_program(wasmBytes, manifestToml));
188
+ },
189
+
190
+ async memoryBytes() {
191
+ return wasm.memory.buffer.byteLength;
192
+ },
193
+
194
+ async openSession() {
195
+ return bindings.pie_open_session();
196
+ },
197
+ async closeSession(id) {
198
+ bindings.pie_close_session(id);
199
+ },
200
+ async sendFrame(id, frame) {
201
+ bindings.pie_send_frame(id, frame);
202
+ scheduleTickAfterMessages();
203
+ },
204
+ async recvFrames(id, maxWaitMs, max) {
205
+ return await driven(bindings.pie_recv_frames(id, maxWaitMs, max));
206
+ },
207
+ };
208
+
209
+ export function transferables(value) {
210
+ if (value instanceof Uint8Array) return [value.buffer];
211
+ if (Array.isArray(value)) return value.filter((v) => v instanceof Uint8Array).map((v) => v.buffer);
212
+ return [];
213
+ }
214
+
215
+ if (typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope) {
216
+ self.onmessage = async ({ data }) => {
217
+ const { id, op, args = [] } = data;
218
+ let lastProgress = 0;
219
+ const progress = (got, total, from) => {
220
+ const now = performance.now();
221
+ if (got < total && now - lastProgress < 50) return;
222
+ lastProgress = now;
223
+ self.postMessage({ id, progress: [got, total, from] });
224
+ };
225
+ try {
226
+ const fn = host[op];
227
+ if (typeof fn !== "function") throw new Error(`pie: unknown op ${op}`);
228
+ const value = await fn.call(host, ...args, progress);
229
+ self.postMessage({ id, ok: true, value }, transferables(value));
230
+ } catch (e) {
231
+ self.postMessage({ id, ok: false, error: String(e?.message ?? e) });
232
+ }
233
+ };
234
+ }