@panyam/tsappkit 0.1.1 → 0.3.0

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.
@@ -0,0 +1,62 @@
1
+ /** A mount's files, keyed by slash-separated path within the mount. */
2
+ type Files = Record<string, Uint8Array>;
3
+
4
+ /** Where the worker's pieces are served, and the global name the Go side exports under. */
5
+ interface StartWorkerOptions {
6
+ /** The built worker script, @panyam/tsappkit/wasmhost/worker.js, as your bundler serves it. */
7
+ worker: string | URL;
8
+ /** The Go wasm build. */
9
+ wasm: string | URL;
10
+ /** Go's wasm_exec.js, from `$(go env GOROOT)/lib/wasm`, matching the Go that built the wasm. */
11
+ exec: string | URL;
12
+ /** The ns the Go program passed to wasmhost.Serve. */
13
+ ns: string;
14
+ }
15
+ /**
16
+ * Starts the worker and resolves with it once the Go program has installed its exports, or rejects
17
+ * with why it couldn't load. Relative URLs resolve against the page, not the worker script.
18
+ */
19
+ declare function startWorker(opts: StartWorkerOptions): Promise<Worker>;
20
+ /**
21
+ * A fetch that answers in the worker, for `createConnectTransport({ baseUrl, fetch: workerFetch(worker) })`.
22
+ * Only the path and query reach the worker, so baseUrl can be anything absolute, such as
23
+ * location.origin. It rejects if the worker reports an error (no handler, a failed build, a handler
24
+ * panic) and resolves with any HTTP status the handler chose.
25
+ */
26
+ declare function workerFetch(worker: Worker): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
27
+ /**
28
+ * Replaces the mount `name` with `files`. The buffers are transferred to the worker, not copied, so
29
+ * each Uint8Array is empty here afterwards; copy first anything the page still needs. Resolves once
30
+ * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).
31
+ */
32
+ declare function mountFiles(worker: Worker, name: string, files: Files): Promise<void>;
33
+ /**
34
+ * Adds `files` to the mount `name`, keeping the files it already holds; a file at a path the mount
35
+ * already has is replaced, and a missing mount is created. This suits a page that brings files in a
36
+ * piece at a time, since it only sends what's new. The worker checks the whole batch first, so a
37
+ * rejection (a path that is already a directory, say) leaves the mount as it was. Buffers are
38
+ * transferred as with mountFiles.
39
+ */
40
+ declare function addFiles(worker: Worker, name: string, files: Files): Promise<void>;
41
+ /**
42
+ * The bytes of linear memory the worker's wasm holds. Wasm memory grows and never shrinks, so this
43
+ * is the peak the engine has needed so far, which is what an app weighs before giving a browser a
44
+ * bigger job.
45
+ */
46
+ declare function workerMemory(worker: Worker): Promise<number>;
47
+ /** Removes the mount `name`. Removing a name that isn't mounted succeeds. */
48
+ declare function unmountFiles(worker: Worker, name: string): Promise<void>;
49
+
50
+ /**
51
+ * Reads a drop into files keyed by path. A dropped folder keeps its own name as the first path
52
+ * element ("design/board.edn"), and dropped loose files sit at the top. Entries the browser can't
53
+ * read as files or folders are skipped.
54
+ */
55
+ declare function filesFromDrop(dt: DataTransfer): Promise<Files>;
56
+ /**
57
+ * Reads an `<input type="file" webkitdirectory>` (or `multiple`) selection into files keyed by
58
+ * path. Folder picks use each file's webkitRelativePath, which starts with the folder's name.
59
+ */
60
+ declare function filesFromFileList(list: FileList | File[]): Promise<Files>;
61
+
62
+ export { type Files, type StartWorkerOptions, addFiles, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch, workerMemory };
@@ -0,0 +1,62 @@
1
+ /** A mount's files, keyed by slash-separated path within the mount. */
2
+ type Files = Record<string, Uint8Array>;
3
+
4
+ /** Where the worker's pieces are served, and the global name the Go side exports under. */
5
+ interface StartWorkerOptions {
6
+ /** The built worker script, @panyam/tsappkit/wasmhost/worker.js, as your bundler serves it. */
7
+ worker: string | URL;
8
+ /** The Go wasm build. */
9
+ wasm: string | URL;
10
+ /** Go's wasm_exec.js, from `$(go env GOROOT)/lib/wasm`, matching the Go that built the wasm. */
11
+ exec: string | URL;
12
+ /** The ns the Go program passed to wasmhost.Serve. */
13
+ ns: string;
14
+ }
15
+ /**
16
+ * Starts the worker and resolves with it once the Go program has installed its exports, or rejects
17
+ * with why it couldn't load. Relative URLs resolve against the page, not the worker script.
18
+ */
19
+ declare function startWorker(opts: StartWorkerOptions): Promise<Worker>;
20
+ /**
21
+ * A fetch that answers in the worker, for `createConnectTransport({ baseUrl, fetch: workerFetch(worker) })`.
22
+ * Only the path and query reach the worker, so baseUrl can be anything absolute, such as
23
+ * location.origin. It rejects if the worker reports an error (no handler, a failed build, a handler
24
+ * panic) and resolves with any HTTP status the handler chose.
25
+ */
26
+ declare function workerFetch(worker: Worker): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
27
+ /**
28
+ * Replaces the mount `name` with `files`. The buffers are transferred to the worker, not copied, so
29
+ * each Uint8Array is empty here afterwards; copy first anything the page still needs. Resolves once
30
+ * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).
31
+ */
32
+ declare function mountFiles(worker: Worker, name: string, files: Files): Promise<void>;
33
+ /**
34
+ * Adds `files` to the mount `name`, keeping the files it already holds; a file at a path the mount
35
+ * already has is replaced, and a missing mount is created. This suits a page that brings files in a
36
+ * piece at a time, since it only sends what's new. The worker checks the whole batch first, so a
37
+ * rejection (a path that is already a directory, say) leaves the mount as it was. Buffers are
38
+ * transferred as with mountFiles.
39
+ */
40
+ declare function addFiles(worker: Worker, name: string, files: Files): Promise<void>;
41
+ /**
42
+ * The bytes of linear memory the worker's wasm holds. Wasm memory grows and never shrinks, so this
43
+ * is the peak the engine has needed so far, which is what an app weighs before giving a browser a
44
+ * bigger job.
45
+ */
46
+ declare function workerMemory(worker: Worker): Promise<number>;
47
+ /** Removes the mount `name`. Removing a name that isn't mounted succeeds. */
48
+ declare function unmountFiles(worker: Worker, name: string): Promise<void>;
49
+
50
+ /**
51
+ * Reads a drop into files keyed by path. A dropped folder keeps its own name as the first path
52
+ * element ("design/board.edn"), and dropped loose files sit at the top. Entries the browser can't
53
+ * read as files or folders are skipped.
54
+ */
55
+ declare function filesFromDrop(dt: DataTransfer): Promise<Files>;
56
+ /**
57
+ * Reads an `<input type="file" webkitdirectory>` (or `multiple`) selection into files keyed by
58
+ * path. Folder picks use each file's webkitRelativePath, which starts with the folder's name.
59
+ */
60
+ declare function filesFromFileList(list: FileList | File[]): Promise<Files>;
61
+
62
+ export { type Files, type StartWorkerOptions, addFiles, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch, workerMemory };
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ // src/wasmhost/client.ts
4
+ var Channel = class {
5
+ constructor(worker) {
6
+ this.worker = worker;
7
+ this.next = 1;
8
+ this.pending = /* @__PURE__ */ new Map();
9
+ this.dead = null;
10
+ worker.addEventListener("message", (ev) => {
11
+ const m = ev.data;
12
+ if ("exited" in m) {
13
+ this.fail(new Error(`wasm host: ${m.exited}`));
14
+ } else if ("id" in m) {
15
+ this.pending.get(m.id)?.resolve(m);
16
+ this.pending.delete(m.id);
17
+ }
18
+ });
19
+ worker.addEventListener("error", (ev) => this.fail(new Error(`wasm host worker: ${ev.message}`)));
20
+ }
21
+ call(req, transfer) {
22
+ if (this.dead) return Promise.reject(this.dead);
23
+ return new Promise((resolve, reject) => {
24
+ const id = this.next++;
25
+ this.pending.set(id, { resolve, reject });
26
+ this.worker.postMessage({ ...req, id }, transfer);
27
+ });
28
+ }
29
+ fail(err) {
30
+ this.dead = err;
31
+ for (const p of this.pending.values()) p.reject(err);
32
+ this.pending.clear();
33
+ }
34
+ };
35
+ var channels = /* @__PURE__ */ new WeakMap();
36
+ function channel(worker) {
37
+ let c = channels.get(worker);
38
+ if (!c) {
39
+ c = new Channel(worker);
40
+ channels.set(worker, c);
41
+ }
42
+ return c;
43
+ }
44
+ function startWorker(opts) {
45
+ const abs = (u) => new URL(u, location.href).href;
46
+ const url = new URL(abs(opts.worker));
47
+ url.searchParams.set("wasm", abs(opts.wasm));
48
+ url.searchParams.set("exec", abs(opts.exec));
49
+ url.searchParams.set("ns", opts.ns);
50
+ const worker = new Worker(url);
51
+ channel(worker);
52
+ return new Promise((resolve, reject) => {
53
+ const onMessage = (ev) => {
54
+ const m = ev.data;
55
+ if (!("ready" in m)) return;
56
+ worker.removeEventListener("message", onMessage);
57
+ if (m.ready) resolve(worker);
58
+ else reject(new Error(`wasm host failed to load: ${m.error}`));
59
+ };
60
+ worker.addEventListener("message", onMessage);
61
+ worker.addEventListener("error", (ev) => reject(new Error(`wasm host worker: ${ev.message}`)), {
62
+ once: true
63
+ });
64
+ });
65
+ }
66
+ var NULL_BODY = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
67
+ function workerFetch(worker) {
68
+ const c = channel(worker);
69
+ return async (input, init) => {
70
+ const req = new Request(input, init);
71
+ const buf = new Uint8Array(await req.arrayBuffer());
72
+ const body = buf.byteLength ? buf : null;
73
+ const headers = {};
74
+ req.headers.forEach((v, k) => headers[k] = v);
75
+ const u = new URL(req.url);
76
+ const reply = await c.call(
77
+ { kind: "http", method: req.method, url: u.pathname + u.search, headers, body },
78
+ body ? [body.buffer] : []
79
+ );
80
+ if (!reply.ok) throw new Error(reply.error);
81
+ const status = reply.status ?? 200;
82
+ const resBody = NULL_BODY.has(status) ? null : reply.body ?? null;
83
+ return new Response(resBody, { status, headers: reply.headers });
84
+ };
85
+ }
86
+ function mountFiles(worker, name, files) {
87
+ return sendFiles(worker, "mount", name, files);
88
+ }
89
+ function addFiles(worker, name, files) {
90
+ return sendFiles(worker, "add", name, files);
91
+ }
92
+ async function sendFiles(worker, kind, name, files) {
93
+ const transfer = /* @__PURE__ */ new Set();
94
+ for (const b of Object.values(files)) {
95
+ if (Object.prototype.toString.call(b.buffer) === "[object ArrayBuffer]") transfer.add(b.buffer);
96
+ }
97
+ const reply = await channel(worker).call({ kind, name, files }, [...transfer]);
98
+ if (!reply.ok) throw new Error(reply.error);
99
+ }
100
+ async function workerMemory(worker) {
101
+ const reply = await channel(worker).call({ kind: "stats" }, []);
102
+ if (!reply.ok) throw new Error(reply.error);
103
+ return reply.memoryBytes ?? 0;
104
+ }
105
+ async function unmountFiles(worker, name) {
106
+ const reply = await channel(worker).call({ kind: "unmount", name }, []);
107
+ if (!reply.ok) throw new Error(reply.error);
108
+ }
109
+
110
+ // src/wasmhost/drop.ts
111
+ async function filesFromDrop(dt) {
112
+ const entries = [];
113
+ const loose = [];
114
+ for (const item of Array.from(dt.items)) {
115
+ if (item.kind !== "file") continue;
116
+ const entry = item.webkitGetAsEntry?.();
117
+ if (entry) entries.push(entry);
118
+ else {
119
+ const f = item.getAsFile();
120
+ if (f) loose.push(f);
121
+ }
122
+ }
123
+ const out = {};
124
+ await Promise.all([
125
+ ...entries.map((e) => readEntry(e, out)),
126
+ ...loose.map(async (f) => {
127
+ out[f.name] = new Uint8Array(await f.arrayBuffer());
128
+ })
129
+ ]);
130
+ return out;
131
+ }
132
+ async function filesFromFileList(list) {
133
+ const out = {};
134
+ await Promise.all(
135
+ Array.from(list).map(async (f) => {
136
+ out[f.webkitRelativePath || f.name] = new Uint8Array(await f.arrayBuffer());
137
+ })
138
+ );
139
+ return out;
140
+ }
141
+ async function readEntry(entry, out) {
142
+ if (entry.isFile) {
143
+ const f = await new Promise((ok, err) => entry.file(ok, err));
144
+ out[entry.fullPath.replace(/^\/+/, "")] = new Uint8Array(await f.arrayBuffer());
145
+ return;
146
+ }
147
+ if (!entry.isDirectory) return;
148
+ const reader = entry.createReader();
149
+ for (; ; ) {
150
+ const batch = await new Promise((ok, err) => reader.readEntries(ok, err));
151
+ if (batch.length === 0) return;
152
+ await Promise.all(batch.map((e) => readEntry(e, out)));
153
+ }
154
+ }
155
+
156
+ exports.addFiles = addFiles;
157
+ exports.filesFromDrop = filesFromDrop;
158
+ exports.filesFromFileList = filesFromFileList;
159
+ exports.mountFiles = mountFiles;
160
+ exports.startWorker = startWorker;
161
+ exports.unmountFiles = unmountFiles;
162
+ exports.workerFetch = workerFetch;
163
+ exports.workerMemory = workerMemory;
164
+ //# sourceMappingURL=index.js.map
165
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/wasmhost/client.ts","../../src/wasmhost/drop.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["// The page's side of a wasm host worker (goapplib issue 32). startWorker loads the app's Go wasm in\n// a Web Worker; workerFetch is a fetch to hand a Connect transport, so the app's generated clients\n// talk to the worker exactly as they would to a server; mountFiles pushes files in before the\n// requests that read them.\nimport type { Files, HostReply, HostRequest, HostStatus } from \"./protocol\";\n\nexport type { Files } from \"./protocol\";\n\n/** Where the worker's pieces are served, and the global name the Go side exports under. */\nexport interface StartWorkerOptions {\n /** The built worker script, @panyam/tsappkit/wasmhost/worker.js, as your bundler serves it. */\n worker: string | URL;\n /** The Go wasm build. */\n wasm: string | URL;\n /** Go's wasm_exec.js, from `$(go env GOROOT)/lib/wasm`, matching the Go that built the wasm. */\n exec: string | URL;\n /** The ns the Go program passed to wasmhost.Serve. */\n ns: string;\n}\n\ntype Unsent = HostRequest extends infer R ? (R extends HostRequest ? Omit<R, \"id\"> : never) : never;\n\nclass Channel {\n private next = 1;\n private pending = new Map<number, { resolve: (r: HostReply) => void; reject: (e: Error) => void }>();\n private dead: Error | null = null;\n\n constructor(private worker: Worker) {\n worker.addEventListener(\"message\", (ev: MessageEvent<HostReply | HostStatus>) => {\n const m = ev.data;\n if (\"exited\" in m) {\n this.fail(new Error(`wasm host: ${m.exited}`));\n } else if (\"id\" in m) {\n this.pending.get(m.id)?.resolve(m);\n this.pending.delete(m.id);\n }\n });\n worker.addEventListener(\"error\", (ev: ErrorEvent) => this.fail(new Error(`wasm host worker: ${ev.message}`)));\n }\n\n call(req: Unsent, transfer: Transferable[]): Promise<HostReply> {\n if (this.dead) return Promise.reject(this.dead);\n return new Promise((resolve, reject) => {\n const id = this.next++;\n this.pending.set(id, { resolve, reject });\n this.worker.postMessage({ ...req, id }, transfer);\n });\n }\n\n private fail(err: Error) {\n this.dead = err;\n for (const p of this.pending.values()) p.reject(err);\n this.pending.clear();\n }\n}\n\nconst channels = new WeakMap<Worker, Channel>();\n\nfunction channel(worker: Worker): Channel {\n let c = channels.get(worker);\n if (!c) {\n c = new Channel(worker);\n channels.set(worker, c);\n }\n return c;\n}\n\n/**\n * Starts the worker and resolves with it once the Go program has installed its exports, or rejects\n * with why it couldn't load. Relative URLs resolve against the page, not the worker script.\n */\nexport function startWorker(opts: StartWorkerOptions): Promise<Worker> {\n const abs = (u: string | URL) => new URL(u, location.href).href;\n const url = new URL(abs(opts.worker));\n url.searchParams.set(\"wasm\", abs(opts.wasm));\n url.searchParams.set(\"exec\", abs(opts.exec));\n url.searchParams.set(\"ns\", opts.ns);\n const worker = new Worker(url);\n channel(worker);\n return new Promise((resolve, reject) => {\n const onMessage = (ev: MessageEvent<HostStatus | HostReply>) => {\n const m = ev.data;\n if (!(\"ready\" in m)) return;\n worker.removeEventListener(\"message\", onMessage);\n if (m.ready) resolve(worker);\n else reject(new Error(`wasm host failed to load: ${m.error}`));\n };\n worker.addEventListener(\"message\", onMessage);\n worker.addEventListener(\"error\", (ev: ErrorEvent) => reject(new Error(`wasm host worker: ${ev.message}`)), {\n once: true,\n });\n });\n}\n\n// Statuses whose Response must have no body.\nconst NULL_BODY = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A fetch that answers in the worker, for `createConnectTransport({ baseUrl, fetch: workerFetch(worker) })`.\n * Only the path and query reach the worker, so baseUrl can be anything absolute, such as\n * location.origin. It rejects if the worker reports an error (no handler, a failed build, a handler\n * panic) and resolves with any HTTP status the handler chose.\n */\nexport function workerFetch(worker: Worker): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {\n const c = channel(worker);\n return async (input, init) => {\n const req = new Request(input, init);\n const buf = new Uint8Array(await req.arrayBuffer());\n const body = buf.byteLength ? buf : null;\n const headers: Record<string, string> = {};\n req.headers.forEach((v, k) => (headers[k] = v));\n const u = new URL(req.url);\n const reply = await c.call(\n { kind: \"http\", method: req.method, url: u.pathname + u.search, headers, body },\n body ? [body.buffer] : [],\n );\n if (!reply.ok) throw new Error(reply.error);\n const status = reply.status ?? 200;\n const resBody = NULL_BODY.has(status) ? null : ((reply.body ?? null) as BodyInit | null);\n return new Response(resBody, { status, headers: reply.headers });\n };\n}\n\n/**\n * Replaces the mount `name` with `files`. The buffers are transferred to the worker, not copied, so\n * each Uint8Array is empty here afterwards; copy first anything the page still needs. Resolves once\n * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).\n */\nexport function mountFiles(worker: Worker, name: string, files: Files): Promise<void> {\n return sendFiles(worker, \"mount\", name, files);\n}\n\n/**\n * Adds `files` to the mount `name`, keeping the files it already holds; a file at a path the mount\n * already has is replaced, and a missing mount is created. This suits a page that brings files in a\n * piece at a time, since it only sends what's new. The worker checks the whole batch first, so a\n * rejection (a path that is already a directory, say) leaves the mount as it was. Buffers are\n * transferred as with mountFiles.\n */\nexport function addFiles(worker: Worker, name: string, files: Files): Promise<void> {\n return sendFiles(worker, \"add\", name, files);\n}\n\nasync function sendFiles(worker: Worker, kind: \"mount\" | \"add\", name: string, files: Files): Promise<void> {\n const transfer = new Set<ArrayBuffer>();\n for (const b of Object.values(files)) {\n // A tag check rather than instanceof, which fails for a buffer from another realm (an iframe),\n // and a SharedArrayBuffer can't be transferred.\n if (Object.prototype.toString.call(b.buffer) === \"[object ArrayBuffer]\") transfer.add(b.buffer as ArrayBuffer);\n }\n const reply = await channel(worker).call({ kind, name, files }, [...transfer]);\n if (!reply.ok) throw new Error(reply.error);\n}\n\n/**\n * The bytes of linear memory the worker's wasm holds. Wasm memory grows and never shrinks, so this\n * is the peak the engine has needed so far, which is what an app weighs before giving a browser a\n * bigger job.\n */\nexport async function workerMemory(worker: Worker): Promise<number> {\n const reply = await channel(worker).call({ kind: \"stats\" }, []);\n if (!reply.ok) throw new Error(reply.error);\n return reply.memoryBytes ?? 0;\n}\n\n/** Removes the mount `name`. Removing a name that isn't mounted succeeds. */\nexport async function unmountFiles(worker: Worker, name: string): Promise<void> {\n const reply = await channel(worker).call({ kind: \"unmount\", name }, []);\n if (!reply.ok) throw new Error(reply.error);\n}\n","// Turning what a user drops or picks into the {path: bytes} map mountFiles takes. The bytes are\n// read here, before any request, because the Go side can't wait on File.arrayBuffer() mid-request.\nimport type { Files } from \"./protocol\";\n\n// The parts of the File and Directory Entries API used here. Chromium, Firefox and Safari all ship\n// it under the webkit prefix.\ninterface Entry {\n isFile: boolean;\n isDirectory: boolean;\n fullPath: string;\n}\ninterface FileEntry extends Entry {\n file(ok: (f: File) => void, err: (e: unknown) => void): void;\n}\ninterface DirectoryEntry extends Entry {\n createReader(): { readEntries(ok: (es: Entry[]) => void, err: (e: unknown) => void): void };\n}\n\n/**\n * Reads a drop into files keyed by path. A dropped folder keeps its own name as the first path\n * element (\"design/board.edn\"), and dropped loose files sit at the top. Entries the browser can't\n * read as files or folders are skipped.\n */\nexport async function filesFromDrop(dt: DataTransfer): Promise<Files> {\n const entries: Entry[] = [];\n const loose: File[] = [];\n for (const item of Array.from(dt.items)) {\n if (item.kind !== \"file\") continue;\n const entry = (item as DataTransferItem & { webkitGetAsEntry?: () => Entry | null }).webkitGetAsEntry?.();\n if (entry) entries.push(entry);\n else {\n const f = item.getAsFile();\n if (f) loose.push(f);\n }\n }\n const out: Files = {};\n await Promise.all([\n ...entries.map((e) => readEntry(e, out)),\n ...loose.map(async (f) => {\n out[f.name] = new Uint8Array(await f.arrayBuffer());\n }),\n ]);\n return out;\n}\n\n/**\n * Reads an `<input type=\"file\" webkitdirectory>` (or `multiple`) selection into files keyed by\n * path. Folder picks use each file's webkitRelativePath, which starts with the folder's name.\n */\nexport async function filesFromFileList(list: FileList | File[]): Promise<Files> {\n const out: Files = {};\n await Promise.all(\n Array.from(list).map(async (f) => {\n out[f.webkitRelativePath || f.name] = new Uint8Array(await f.arrayBuffer());\n }),\n );\n return out;\n}\n\nasync function readEntry(entry: Entry, out: Files): Promise<void> {\n if (entry.isFile) {\n const f = await new Promise<File>((ok, err) => (entry as FileEntry).file(ok, err));\n out[entry.fullPath.replace(/^\\/+/, \"\")] = new Uint8Array(await f.arrayBuffer());\n return;\n }\n if (!entry.isDirectory) return;\n const reader = (entry as DirectoryEntry).createReader();\n // readEntries returns a batch at a time (100 in Chromium) and an empty batch at the end.\n for (;;) {\n const batch = await new Promise<Entry[]>((ok, err) => reader.readEntries(ok, err));\n if (batch.length === 0) return;\n await Promise.all(batch.map((e) => readEntry(e, out)));\n }\n}\n"]}
@@ -0,0 +1,156 @@
1
+ // src/wasmhost/client.ts
2
+ var Channel = class {
3
+ constructor(worker) {
4
+ this.worker = worker;
5
+ this.next = 1;
6
+ this.pending = /* @__PURE__ */ new Map();
7
+ this.dead = null;
8
+ worker.addEventListener("message", (ev) => {
9
+ const m = ev.data;
10
+ if ("exited" in m) {
11
+ this.fail(new Error(`wasm host: ${m.exited}`));
12
+ } else if ("id" in m) {
13
+ this.pending.get(m.id)?.resolve(m);
14
+ this.pending.delete(m.id);
15
+ }
16
+ });
17
+ worker.addEventListener("error", (ev) => this.fail(new Error(`wasm host worker: ${ev.message}`)));
18
+ }
19
+ call(req, transfer) {
20
+ if (this.dead) return Promise.reject(this.dead);
21
+ return new Promise((resolve, reject) => {
22
+ const id = this.next++;
23
+ this.pending.set(id, { resolve, reject });
24
+ this.worker.postMessage({ ...req, id }, transfer);
25
+ });
26
+ }
27
+ fail(err) {
28
+ this.dead = err;
29
+ for (const p of this.pending.values()) p.reject(err);
30
+ this.pending.clear();
31
+ }
32
+ };
33
+ var channels = /* @__PURE__ */ new WeakMap();
34
+ function channel(worker) {
35
+ let c = channels.get(worker);
36
+ if (!c) {
37
+ c = new Channel(worker);
38
+ channels.set(worker, c);
39
+ }
40
+ return c;
41
+ }
42
+ function startWorker(opts) {
43
+ const abs = (u) => new URL(u, location.href).href;
44
+ const url = new URL(abs(opts.worker));
45
+ url.searchParams.set("wasm", abs(opts.wasm));
46
+ url.searchParams.set("exec", abs(opts.exec));
47
+ url.searchParams.set("ns", opts.ns);
48
+ const worker = new Worker(url);
49
+ channel(worker);
50
+ return new Promise((resolve, reject) => {
51
+ const onMessage = (ev) => {
52
+ const m = ev.data;
53
+ if (!("ready" in m)) return;
54
+ worker.removeEventListener("message", onMessage);
55
+ if (m.ready) resolve(worker);
56
+ else reject(new Error(`wasm host failed to load: ${m.error}`));
57
+ };
58
+ worker.addEventListener("message", onMessage);
59
+ worker.addEventListener("error", (ev) => reject(new Error(`wasm host worker: ${ev.message}`)), {
60
+ once: true
61
+ });
62
+ });
63
+ }
64
+ var NULL_BODY = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
65
+ function workerFetch(worker) {
66
+ const c = channel(worker);
67
+ return async (input, init) => {
68
+ const req = new Request(input, init);
69
+ const buf = new Uint8Array(await req.arrayBuffer());
70
+ const body = buf.byteLength ? buf : null;
71
+ const headers = {};
72
+ req.headers.forEach((v, k) => headers[k] = v);
73
+ const u = new URL(req.url);
74
+ const reply = await c.call(
75
+ { kind: "http", method: req.method, url: u.pathname + u.search, headers, body },
76
+ body ? [body.buffer] : []
77
+ );
78
+ if (!reply.ok) throw new Error(reply.error);
79
+ const status = reply.status ?? 200;
80
+ const resBody = NULL_BODY.has(status) ? null : reply.body ?? null;
81
+ return new Response(resBody, { status, headers: reply.headers });
82
+ };
83
+ }
84
+ function mountFiles(worker, name, files) {
85
+ return sendFiles(worker, "mount", name, files);
86
+ }
87
+ function addFiles(worker, name, files) {
88
+ return sendFiles(worker, "add", name, files);
89
+ }
90
+ async function sendFiles(worker, kind, name, files) {
91
+ const transfer = /* @__PURE__ */ new Set();
92
+ for (const b of Object.values(files)) {
93
+ if (Object.prototype.toString.call(b.buffer) === "[object ArrayBuffer]") transfer.add(b.buffer);
94
+ }
95
+ const reply = await channel(worker).call({ kind, name, files }, [...transfer]);
96
+ if (!reply.ok) throw new Error(reply.error);
97
+ }
98
+ async function workerMemory(worker) {
99
+ const reply = await channel(worker).call({ kind: "stats" }, []);
100
+ if (!reply.ok) throw new Error(reply.error);
101
+ return reply.memoryBytes ?? 0;
102
+ }
103
+ async function unmountFiles(worker, name) {
104
+ const reply = await channel(worker).call({ kind: "unmount", name }, []);
105
+ if (!reply.ok) throw new Error(reply.error);
106
+ }
107
+
108
+ // src/wasmhost/drop.ts
109
+ async function filesFromDrop(dt) {
110
+ const entries = [];
111
+ const loose = [];
112
+ for (const item of Array.from(dt.items)) {
113
+ if (item.kind !== "file") continue;
114
+ const entry = item.webkitGetAsEntry?.();
115
+ if (entry) entries.push(entry);
116
+ else {
117
+ const f = item.getAsFile();
118
+ if (f) loose.push(f);
119
+ }
120
+ }
121
+ const out = {};
122
+ await Promise.all([
123
+ ...entries.map((e) => readEntry(e, out)),
124
+ ...loose.map(async (f) => {
125
+ out[f.name] = new Uint8Array(await f.arrayBuffer());
126
+ })
127
+ ]);
128
+ return out;
129
+ }
130
+ async function filesFromFileList(list) {
131
+ const out = {};
132
+ await Promise.all(
133
+ Array.from(list).map(async (f) => {
134
+ out[f.webkitRelativePath || f.name] = new Uint8Array(await f.arrayBuffer());
135
+ })
136
+ );
137
+ return out;
138
+ }
139
+ async function readEntry(entry, out) {
140
+ if (entry.isFile) {
141
+ const f = await new Promise((ok, err) => entry.file(ok, err));
142
+ out[entry.fullPath.replace(/^\/+/, "")] = new Uint8Array(await f.arrayBuffer());
143
+ return;
144
+ }
145
+ if (!entry.isDirectory) return;
146
+ const reader = entry.createReader();
147
+ for (; ; ) {
148
+ const batch = await new Promise((ok, err) => reader.readEntries(ok, err));
149
+ if (batch.length === 0) return;
150
+ await Promise.all(batch.map((e) => readEntry(e, out)));
151
+ }
152
+ }
153
+
154
+ export { addFiles, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch, workerMemory };
155
+ //# sourceMappingURL=index.mjs.map
156
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/wasmhost/client.ts","../../src/wasmhost/drop.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["// The page's side of a wasm host worker (goapplib issue 32). startWorker loads the app's Go wasm in\n// a Web Worker; workerFetch is a fetch to hand a Connect transport, so the app's generated clients\n// talk to the worker exactly as they would to a server; mountFiles pushes files in before the\n// requests that read them.\nimport type { Files, HostReply, HostRequest, HostStatus } from \"./protocol\";\n\nexport type { Files } from \"./protocol\";\n\n/** Where the worker's pieces are served, and the global name the Go side exports under. */\nexport interface StartWorkerOptions {\n /** The built worker script, @panyam/tsappkit/wasmhost/worker.js, as your bundler serves it. */\n worker: string | URL;\n /** The Go wasm build. */\n wasm: string | URL;\n /** Go's wasm_exec.js, from `$(go env GOROOT)/lib/wasm`, matching the Go that built the wasm. */\n exec: string | URL;\n /** The ns the Go program passed to wasmhost.Serve. */\n ns: string;\n}\n\ntype Unsent = HostRequest extends infer R ? (R extends HostRequest ? Omit<R, \"id\"> : never) : never;\n\nclass Channel {\n private next = 1;\n private pending = new Map<number, { resolve: (r: HostReply) => void; reject: (e: Error) => void }>();\n private dead: Error | null = null;\n\n constructor(private worker: Worker) {\n worker.addEventListener(\"message\", (ev: MessageEvent<HostReply | HostStatus>) => {\n const m = ev.data;\n if (\"exited\" in m) {\n this.fail(new Error(`wasm host: ${m.exited}`));\n } else if (\"id\" in m) {\n this.pending.get(m.id)?.resolve(m);\n this.pending.delete(m.id);\n }\n });\n worker.addEventListener(\"error\", (ev: ErrorEvent) => this.fail(new Error(`wasm host worker: ${ev.message}`)));\n }\n\n call(req: Unsent, transfer: Transferable[]): Promise<HostReply> {\n if (this.dead) return Promise.reject(this.dead);\n return new Promise((resolve, reject) => {\n const id = this.next++;\n this.pending.set(id, { resolve, reject });\n this.worker.postMessage({ ...req, id }, transfer);\n });\n }\n\n private fail(err: Error) {\n this.dead = err;\n for (const p of this.pending.values()) p.reject(err);\n this.pending.clear();\n }\n}\n\nconst channels = new WeakMap<Worker, Channel>();\n\nfunction channel(worker: Worker): Channel {\n let c = channels.get(worker);\n if (!c) {\n c = new Channel(worker);\n channels.set(worker, c);\n }\n return c;\n}\n\n/**\n * Starts the worker and resolves with it once the Go program has installed its exports, or rejects\n * with why it couldn't load. Relative URLs resolve against the page, not the worker script.\n */\nexport function startWorker(opts: StartWorkerOptions): Promise<Worker> {\n const abs = (u: string | URL) => new URL(u, location.href).href;\n const url = new URL(abs(opts.worker));\n url.searchParams.set(\"wasm\", abs(opts.wasm));\n url.searchParams.set(\"exec\", abs(opts.exec));\n url.searchParams.set(\"ns\", opts.ns);\n const worker = new Worker(url);\n channel(worker);\n return new Promise((resolve, reject) => {\n const onMessage = (ev: MessageEvent<HostStatus | HostReply>) => {\n const m = ev.data;\n if (!(\"ready\" in m)) return;\n worker.removeEventListener(\"message\", onMessage);\n if (m.ready) resolve(worker);\n else reject(new Error(`wasm host failed to load: ${m.error}`));\n };\n worker.addEventListener(\"message\", onMessage);\n worker.addEventListener(\"error\", (ev: ErrorEvent) => reject(new Error(`wasm host worker: ${ev.message}`)), {\n once: true,\n });\n });\n}\n\n// Statuses whose Response must have no body.\nconst NULL_BODY = new Set([101, 103, 204, 205, 304]);\n\n/**\n * A fetch that answers in the worker, for `createConnectTransport({ baseUrl, fetch: workerFetch(worker) })`.\n * Only the path and query reach the worker, so baseUrl can be anything absolute, such as\n * location.origin. It rejects if the worker reports an error (no handler, a failed build, a handler\n * panic) and resolves with any HTTP status the handler chose.\n */\nexport function workerFetch(worker: Worker): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {\n const c = channel(worker);\n return async (input, init) => {\n const req = new Request(input, init);\n const buf = new Uint8Array(await req.arrayBuffer());\n const body = buf.byteLength ? buf : null;\n const headers: Record<string, string> = {};\n req.headers.forEach((v, k) => (headers[k] = v));\n const u = new URL(req.url);\n const reply = await c.call(\n { kind: \"http\", method: req.method, url: u.pathname + u.search, headers, body },\n body ? [body.buffer] : [],\n );\n if (!reply.ok) throw new Error(reply.error);\n const status = reply.status ?? 200;\n const resBody = NULL_BODY.has(status) ? null : ((reply.body ?? null) as BodyInit | null);\n return new Response(resBody, { status, headers: reply.headers });\n };\n}\n\n/**\n * Replaces the mount `name` with `files`. The buffers are transferred to the worker, not copied, so\n * each Uint8Array is empty here afterwards; copy first anything the page still needs. Resolves once\n * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).\n */\nexport function mountFiles(worker: Worker, name: string, files: Files): Promise<void> {\n return sendFiles(worker, \"mount\", name, files);\n}\n\n/**\n * Adds `files` to the mount `name`, keeping the files it already holds; a file at a path the mount\n * already has is replaced, and a missing mount is created. This suits a page that brings files in a\n * piece at a time, since it only sends what's new. The worker checks the whole batch first, so a\n * rejection (a path that is already a directory, say) leaves the mount as it was. Buffers are\n * transferred as with mountFiles.\n */\nexport function addFiles(worker: Worker, name: string, files: Files): Promise<void> {\n return sendFiles(worker, \"add\", name, files);\n}\n\nasync function sendFiles(worker: Worker, kind: \"mount\" | \"add\", name: string, files: Files): Promise<void> {\n const transfer = new Set<ArrayBuffer>();\n for (const b of Object.values(files)) {\n // A tag check rather than instanceof, which fails for a buffer from another realm (an iframe),\n // and a SharedArrayBuffer can't be transferred.\n if (Object.prototype.toString.call(b.buffer) === \"[object ArrayBuffer]\") transfer.add(b.buffer as ArrayBuffer);\n }\n const reply = await channel(worker).call({ kind, name, files }, [...transfer]);\n if (!reply.ok) throw new Error(reply.error);\n}\n\n/**\n * The bytes of linear memory the worker's wasm holds. Wasm memory grows and never shrinks, so this\n * is the peak the engine has needed so far, which is what an app weighs before giving a browser a\n * bigger job.\n */\nexport async function workerMemory(worker: Worker): Promise<number> {\n const reply = await channel(worker).call({ kind: \"stats\" }, []);\n if (!reply.ok) throw new Error(reply.error);\n return reply.memoryBytes ?? 0;\n}\n\n/** Removes the mount `name`. Removing a name that isn't mounted succeeds. */\nexport async function unmountFiles(worker: Worker, name: string): Promise<void> {\n const reply = await channel(worker).call({ kind: \"unmount\", name }, []);\n if (!reply.ok) throw new Error(reply.error);\n}\n","// Turning what a user drops or picks into the {path: bytes} map mountFiles takes. The bytes are\n// read here, before any request, because the Go side can't wait on File.arrayBuffer() mid-request.\nimport type { Files } from \"./protocol\";\n\n// The parts of the File and Directory Entries API used here. Chromium, Firefox and Safari all ship\n// it under the webkit prefix.\ninterface Entry {\n isFile: boolean;\n isDirectory: boolean;\n fullPath: string;\n}\ninterface FileEntry extends Entry {\n file(ok: (f: File) => void, err: (e: unknown) => void): void;\n}\ninterface DirectoryEntry extends Entry {\n createReader(): { readEntries(ok: (es: Entry[]) => void, err: (e: unknown) => void): void };\n}\n\n/**\n * Reads a drop into files keyed by path. A dropped folder keeps its own name as the first path\n * element (\"design/board.edn\"), and dropped loose files sit at the top. Entries the browser can't\n * read as files or folders are skipped.\n */\nexport async function filesFromDrop(dt: DataTransfer): Promise<Files> {\n const entries: Entry[] = [];\n const loose: File[] = [];\n for (const item of Array.from(dt.items)) {\n if (item.kind !== \"file\") continue;\n const entry = (item as DataTransferItem & { webkitGetAsEntry?: () => Entry | null }).webkitGetAsEntry?.();\n if (entry) entries.push(entry);\n else {\n const f = item.getAsFile();\n if (f) loose.push(f);\n }\n }\n const out: Files = {};\n await Promise.all([\n ...entries.map((e) => readEntry(e, out)),\n ...loose.map(async (f) => {\n out[f.name] = new Uint8Array(await f.arrayBuffer());\n }),\n ]);\n return out;\n}\n\n/**\n * Reads an `<input type=\"file\" webkitdirectory>` (or `multiple`) selection into files keyed by\n * path. Folder picks use each file's webkitRelativePath, which starts with the folder's name.\n */\nexport async function filesFromFileList(list: FileList | File[]): Promise<Files> {\n const out: Files = {};\n await Promise.all(\n Array.from(list).map(async (f) => {\n out[f.webkitRelativePath || f.name] = new Uint8Array(await f.arrayBuffer());\n }),\n );\n return out;\n}\n\nasync function readEntry(entry: Entry, out: Files): Promise<void> {\n if (entry.isFile) {\n const f = await new Promise<File>((ok, err) => (entry as FileEntry).file(ok, err));\n out[entry.fullPath.replace(/^\\/+/, \"\")] = new Uint8Array(await f.arrayBuffer());\n return;\n }\n if (!entry.isDirectory) return;\n const reader = (entry as DirectoryEntry).createReader();\n // readEntries returns a batch at a time (100 in Chromium) and an empty batch at the end.\n for (;;) {\n const batch = await new Promise<Entry[]>((ok, err) => reader.readEntries(ok, err));\n if (batch.length === 0) return;\n await Promise.all(batch.map((e) => readEntry(e, out)));\n }\n}\n"]}
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+ (() => {
3
+ // src/wasmhost/worker.ts
4
+ var params = new URLSearchParams(self.location.search);
5
+ var wasmUrl = params.get("wasm") ?? "app.wasm";
6
+ var execUrl = params.get("exec") ?? "wasm_exec.js";
7
+ var ns = params.get("ns") ?? "wasmhost";
8
+ var memory;
9
+ var post = (m, transfer = []) => self.postMessage(m, transfer);
10
+ async function boot() {
11
+ importScripts(execUrl);
12
+ const go = new self.Go();
13
+ const ready = new Promise((resolve) => {
14
+ self[ns] = { ready: resolve };
15
+ });
16
+ const res = await fetch(wasmUrl);
17
+ if (!res.ok) throw new Error(`fetch ${wasmUrl}: ${res.status}`);
18
+ const { instance } = res.headers.get("content-type")?.startsWith("application/wasm") ? await WebAssembly.instantiateStreaming(res, go.importObject) : await WebAssembly.instantiate(await res.arrayBuffer(), go.importObject);
19
+ memory = instance.exports.mem;
20
+ void go.run(instance).then(() => post({ exited: "the Go program exited" }));
21
+ await ready;
22
+ return self[ns];
23
+ }
24
+ var booted = boot();
25
+ booted.then(
26
+ () => post({ ready: true }),
27
+ (err) => post({ ready: false, error: String(err) })
28
+ );
29
+ self.onmessage = async (ev) => {
30
+ const req = ev.data;
31
+ try {
32
+ const host = await booted;
33
+ if (req.kind === "http") {
34
+ const res = await host.http(req.method, req.url, req.headers, req.body);
35
+ post({ id: req.id, ok: true, status: res.status, headers: res.headers, body: res.body }, [res.body.buffer]);
36
+ } else if (req.kind === "mount") {
37
+ await host.mount(req.name, req.files);
38
+ post({ id: req.id, ok: true });
39
+ } else if (req.kind === "add") {
40
+ await host.add(req.name, req.files);
41
+ post({ id: req.id, ok: true });
42
+ } else if (req.kind === "stats") {
43
+ post({ id: req.id, ok: true, memoryBytes: memory?.buffer.byteLength ?? 0 });
44
+ } else if (req.kind === "unmount") {
45
+ await host.unmount(req.name);
46
+ post({ id: req.id, ok: true });
47
+ } else {
48
+ throw new Error(`unknown request kind ${JSON.stringify(req.kind)}`);
49
+ }
50
+ } catch (err) {
51
+ post({ id: req.id, ok: false, error: err instanceof Error ? err.message : String(err) });
52
+ }
53
+ };
54
+ })();
55
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/wasmhost/worker.ts"],"sourcesContent":["// The wasm host worker. It loads a Go wasm build that calls goapplib's wasmhost.Serve (or\n// ServeRebuild) and forwards the page's messages to the functions that build exports on\n// globalThis[ns]. Running the Go program here, off the page's thread, keeps a request that takes\n// seconds from freezing the page.\n//\n// It is a classic worker, built as an IIFE, because Go's wasm_exec.js is a plain script defining\n// globalThis.Go, which importScripts can load and a module worker cannot. The asset URLs and the\n// namespace come in the worker's query string (?wasm=&exec=&ns=), which startWorker writes.\nimport type { Files, HostReply, HostRequest, HostStatus } from \"./protocol\";\n\ninterface Exports {\n ready?: () => void;\n http(\n method: string,\n url: string,\n headers: Record<string, string>,\n body: Uint8Array | null,\n ): Promise<{ status: number; headers: Record<string, string>; body: Uint8Array }>;\n mount(name: string, files: Files): Promise<void>;\n add(name: string, files: Files): Promise<void>;\n unmount(name: string): Promise<void>;\n}\n\n// The project compiles against the DOM lib, which describes a window, and the WebWorker lib can't\n// be loaded beside it, so the few worker globals used here are declared.\ndeclare function importScripts(...urls: string[]): void;\ndeclare const self: {\n location: Location;\n postMessage(message: unknown, transfer?: Transferable[]): void;\n onmessage: ((ev: MessageEvent<HostRequest>) => void) | null;\n Go: new () => { importObject: WebAssembly.Imports; run(i: WebAssembly.Instance): Promise<void> };\n} & Record<string, unknown>;\n\nconst params = new URLSearchParams(self.location.search);\nconst wasmUrl = params.get(\"wasm\") ?? \"app.wasm\";\nconst execUrl = params.get(\"exec\") ?? \"wasm_exec.js\";\nconst ns = params.get(\"ns\") ?? \"wasmhost\";\n\n// The wasm's linear memory, exported by Go as `mem`. It only grows, so its size is the peak so far.\nlet memory: WebAssembly.Memory | undefined;\n\nconst post = (m: HostStatus | HostReply, transfer: Transferable[] = []) => self.postMessage(m, transfer);\n\nasync function boot(): Promise<Exports> {\n importScripts(execUrl);\n const go = new self.Go();\n const ready = new Promise<void>((resolve) => {\n self[ns] = { ready: resolve };\n });\n const res = await fetch(wasmUrl);\n if (!res.ok) throw new Error(`fetch ${wasmUrl}: ${res.status}`);\n // instantiateStreaming insists on the application/wasm content type, which not every static\n // server sends.\n const { instance } = res.headers.get(\"content-type\")?.startsWith(\"application/wasm\")\n ? await WebAssembly.instantiateStreaming(res, go.importObject)\n : await WebAssembly.instantiate(await res.arrayBuffer(), go.importObject);\n memory = instance.exports.mem as WebAssembly.Memory | undefined;\n void go.run(instance).then(() => post({ exited: \"the Go program exited\" }));\n await ready;\n return self[ns] as Exports;\n}\n\nconst booted = boot();\nbooted.then(\n () => post({ ready: true }),\n (err: unknown) => post({ ready: false, error: String(err) }),\n);\n\nself.onmessage = async (ev) => {\n const req = ev.data;\n try {\n const host = await booted;\n if (req.kind === \"http\") {\n const res = await host.http(req.method, req.url, req.headers, req.body);\n post({ id: req.id, ok: true, status: res.status, headers: res.headers, body: res.body }, [res.body.buffer]);\n } else if (req.kind === \"mount\") {\n await host.mount(req.name, req.files);\n post({ id: req.id, ok: true });\n } else if (req.kind === \"add\") {\n await host.add(req.name, req.files);\n post({ id: req.id, ok: true });\n } else if (req.kind === \"stats\") {\n post({ id: req.id, ok: true, memoryBytes: memory?.buffer.byteLength ?? 0 });\n } else if (req.kind === \"unmount\") {\n await host.unmount(req.name);\n post({ id: req.id, ok: true });\n } else {\n // A page newer than this worker can send a kind it doesn't know; say so rather than guess.\n throw new Error(`unknown request kind ${JSON.stringify((req as { kind: unknown }).kind)}`);\n }\n } catch (err) {\n post({ id: req.id, ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n};\n"],"mappings":";;;AAiCA,MAAM,SAAS,IAAI,gBAAgB,KAAK,SAAS,MAAM;AACvD,MAAM,UAAU,OAAO,IAAI,MAAM,KAAK;AACtC,MAAM,UAAU,OAAO,IAAI,MAAM,KAAK;AACtC,MAAM,KAAK,OAAO,IAAI,IAAI,KAAK;AAG/B,MAAI;AAEJ,MAAM,OAAO,CAAC,GAA2B,WAA2B,CAAC,MAAM,KAAK,YAAY,GAAG,QAAQ;AAEvG,iBAAe,OAAyB;AACtC,kBAAc,OAAO;AACrB,UAAM,KAAK,IAAI,KAAK,GAAG;AACvB,UAAM,QAAQ,IAAI,QAAc,CAAC,YAAY;AAC3C,WAAK,EAAE,IAAI,EAAE,OAAO,QAAQ;AAAA,IAC9B,CAAC;AACD,UAAM,MAAM,MAAM,MAAM,OAAO;AAC/B,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,SAAS,OAAO,KAAK,IAAI,MAAM,EAAE;AAG9D,UAAM,EAAE,SAAS,IAAI,IAAI,QAAQ,IAAI,cAAc,GAAG,WAAW,kBAAkB,IAC/E,MAAM,YAAY,qBAAqB,KAAK,GAAG,YAAY,IAC3D,MAAM,YAAY,YAAY,MAAM,IAAI,YAAY,GAAG,GAAG,YAAY;AAC1E,aAAS,SAAS,QAAQ;AAC1B,SAAK,GAAG,IAAI,QAAQ,EAAE,KAAK,MAAM,KAAK,EAAE,QAAQ,wBAAwB,CAAC,CAAC;AAC1E,UAAM;AACN,WAAO,KAAK,EAAE;AAAA,EAChB;AAEA,MAAM,SAAS,KAAK;AACpB,SAAO;AAAA,IACL,MAAM,KAAK,EAAE,OAAO,KAAK,CAAC;AAAA,IAC1B,CAAC,QAAiB,KAAK,EAAE,OAAO,OAAO,OAAO,OAAO,GAAG,EAAE,CAAC;AAAA,EAC7D;AAEA,OAAK,YAAY,OAAO,OAAO;AAC7B,UAAM,MAAM,GAAG;AACf,QAAI;AACF,YAAM,OAAO,MAAM;AACnB,UAAI,IAAI,SAAS,QAAQ;AACvB,cAAM,MAAM,MAAM,KAAK,KAAK,IAAI,QAAQ,IAAI,KAAK,IAAI,SAAS,IAAI,IAAI;AACtE,aAAK,EAAE,IAAI,IAAI,IAAI,IAAI,MAAM,QAAQ,IAAI,QAAQ,SAAS,IAAI,SAAS,MAAM,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,MAAM,CAAC;AAAA,MAC5G,WAAW,IAAI,SAAS,SAAS;AAC/B,cAAM,KAAK,MAAM,IAAI,MAAM,IAAI,KAAK;AACpC,aAAK,EAAE,IAAI,IAAI,IAAI,IAAI,KAAK,CAAC;AAAA,MAC/B,WAAW,IAAI,SAAS,OAAO;AAC7B,cAAM,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK;AAClC,aAAK,EAAE,IAAI,IAAI,IAAI,IAAI,KAAK,CAAC;AAAA,MAC/B,WAAW,IAAI,SAAS,SAAS;AAC/B,aAAK,EAAE,IAAI,IAAI,IAAI,IAAI,MAAM,aAAa,QAAQ,OAAO,cAAc,EAAE,CAAC;AAAA,MAC5E,WAAW,IAAI,SAAS,WAAW;AACjC,cAAM,KAAK,QAAQ,IAAI,IAAI;AAC3B,aAAK,EAAE,IAAI,IAAI,IAAI,IAAI,KAAK,CAAC;AAAA,MAC/B,OAAO;AAEL,cAAM,IAAI,MAAM,wBAAwB,KAAK,UAAW,IAA0B,IAAI,CAAC,EAAE;AAAA,MAC3F;AAAA,IACF,SAAS,KAAK;AACZ,WAAK,EAAE,IAAI,IAAI,IAAI,IAAI,OAAO,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC;AAAA,IACzF;AAAA,EACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panyam/tsappkit",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "TypeScript application toolkit with component lifecycle management, event system, UI utilities, and documentation site components",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -16,6 +16,12 @@
16
16
  "import": "./dist/docs/index.mjs",
17
17
  "require": "./dist/docs/index.js"
18
18
  },
19
+ "./wasmhost": {
20
+ "types": "./dist/wasmhost/index.d.ts",
21
+ "import": "./dist/wasmhost/index.mjs",
22
+ "require": "./dist/wasmhost/index.js"
23
+ },
24
+ "./wasmhost/worker.js": "./dist/wasmhost/worker.js",
19
25
  "./styles/*": "./styles/*",
20
26
  "./dist/*": "./dist/*"
21
27
  },
@@ -65,6 +71,7 @@
65
71
  },
66
72
  "repository": {
67
73
  "type": "git",
68
- "url": ""
74
+ "url": "git+https://github.com/panyam/goapplib.git",
75
+ "directory": "tsappkit"
69
76
  }
70
77
  }
@@ -0,0 +1,170 @@
1
+ // The page's side of a wasm host worker (goapplib issue 32). startWorker loads the app's Go wasm in
2
+ // a Web Worker; workerFetch is a fetch to hand a Connect transport, so the app's generated clients
3
+ // talk to the worker exactly as they would to a server; mountFiles pushes files in before the
4
+ // requests that read them.
5
+ import type { Files, HostReply, HostRequest, HostStatus } from "./protocol";
6
+
7
+ export type { Files } from "./protocol";
8
+
9
+ /** Where the worker's pieces are served, and the global name the Go side exports under. */
10
+ export interface StartWorkerOptions {
11
+ /** The built worker script, @panyam/tsappkit/wasmhost/worker.js, as your bundler serves it. */
12
+ worker: string | URL;
13
+ /** The Go wasm build. */
14
+ wasm: string | URL;
15
+ /** Go's wasm_exec.js, from `$(go env GOROOT)/lib/wasm`, matching the Go that built the wasm. */
16
+ exec: string | URL;
17
+ /** The ns the Go program passed to wasmhost.Serve. */
18
+ ns: string;
19
+ }
20
+
21
+ type Unsent = HostRequest extends infer R ? (R extends HostRequest ? Omit<R, "id"> : never) : never;
22
+
23
+ class Channel {
24
+ private next = 1;
25
+ private pending = new Map<number, { resolve: (r: HostReply) => void; reject: (e: Error) => void }>();
26
+ private dead: Error | null = null;
27
+
28
+ constructor(private worker: Worker) {
29
+ worker.addEventListener("message", (ev: MessageEvent<HostReply | HostStatus>) => {
30
+ const m = ev.data;
31
+ if ("exited" in m) {
32
+ this.fail(new Error(`wasm host: ${m.exited}`));
33
+ } else if ("id" in m) {
34
+ this.pending.get(m.id)?.resolve(m);
35
+ this.pending.delete(m.id);
36
+ }
37
+ });
38
+ worker.addEventListener("error", (ev: ErrorEvent) => this.fail(new Error(`wasm host worker: ${ev.message}`)));
39
+ }
40
+
41
+ call(req: Unsent, transfer: Transferable[]): Promise<HostReply> {
42
+ if (this.dead) return Promise.reject(this.dead);
43
+ return new Promise((resolve, reject) => {
44
+ const id = this.next++;
45
+ this.pending.set(id, { resolve, reject });
46
+ this.worker.postMessage({ ...req, id }, transfer);
47
+ });
48
+ }
49
+
50
+ private fail(err: Error) {
51
+ this.dead = err;
52
+ for (const p of this.pending.values()) p.reject(err);
53
+ this.pending.clear();
54
+ }
55
+ }
56
+
57
+ const channels = new WeakMap<Worker, Channel>();
58
+
59
+ function channel(worker: Worker): Channel {
60
+ let c = channels.get(worker);
61
+ if (!c) {
62
+ c = new Channel(worker);
63
+ channels.set(worker, c);
64
+ }
65
+ return c;
66
+ }
67
+
68
+ /**
69
+ * Starts the worker and resolves with it once the Go program has installed its exports, or rejects
70
+ * with why it couldn't load. Relative URLs resolve against the page, not the worker script.
71
+ */
72
+ export function startWorker(opts: StartWorkerOptions): Promise<Worker> {
73
+ const abs = (u: string | URL) => new URL(u, location.href).href;
74
+ const url = new URL(abs(opts.worker));
75
+ url.searchParams.set("wasm", abs(opts.wasm));
76
+ url.searchParams.set("exec", abs(opts.exec));
77
+ url.searchParams.set("ns", opts.ns);
78
+ const worker = new Worker(url);
79
+ channel(worker);
80
+ return new Promise((resolve, reject) => {
81
+ const onMessage = (ev: MessageEvent<HostStatus | HostReply>) => {
82
+ const m = ev.data;
83
+ if (!("ready" in m)) return;
84
+ worker.removeEventListener("message", onMessage);
85
+ if (m.ready) resolve(worker);
86
+ else reject(new Error(`wasm host failed to load: ${m.error}`));
87
+ };
88
+ worker.addEventListener("message", onMessage);
89
+ worker.addEventListener("error", (ev: ErrorEvent) => reject(new Error(`wasm host worker: ${ev.message}`)), {
90
+ once: true,
91
+ });
92
+ });
93
+ }
94
+
95
+ // Statuses whose Response must have no body.
96
+ const NULL_BODY = new Set([101, 103, 204, 205, 304]);
97
+
98
+ /**
99
+ * A fetch that answers in the worker, for `createConnectTransport({ baseUrl, fetch: workerFetch(worker) })`.
100
+ * Only the path and query reach the worker, so baseUrl can be anything absolute, such as
101
+ * location.origin. It rejects if the worker reports an error (no handler, a failed build, a handler
102
+ * panic) and resolves with any HTTP status the handler chose.
103
+ */
104
+ export function workerFetch(worker: Worker): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> {
105
+ const c = channel(worker);
106
+ return async (input, init) => {
107
+ const req = new Request(input, init);
108
+ const buf = new Uint8Array(await req.arrayBuffer());
109
+ const body = buf.byteLength ? buf : null;
110
+ const headers: Record<string, string> = {};
111
+ req.headers.forEach((v, k) => (headers[k] = v));
112
+ const u = new URL(req.url);
113
+ const reply = await c.call(
114
+ { kind: "http", method: req.method, url: u.pathname + u.search, headers, body },
115
+ body ? [body.buffer] : [],
116
+ );
117
+ if (!reply.ok) throw new Error(reply.error);
118
+ const status = reply.status ?? 200;
119
+ const resBody = NULL_BODY.has(status) ? null : ((reply.body ?? null) as BodyInit | null);
120
+ return new Response(resBody, { status, headers: reply.headers });
121
+ };
122
+ }
123
+
124
+ /**
125
+ * Replaces the mount `name` with `files`. The buffers are transferred to the worker, not copied, so
126
+ * each Uint8Array is empty here afterwards; copy first anything the page still needs. Resolves once
127
+ * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).
128
+ */
129
+ export function mountFiles(worker: Worker, name: string, files: Files): Promise<void> {
130
+ return sendFiles(worker, "mount", name, files);
131
+ }
132
+
133
+ /**
134
+ * Adds `files` to the mount `name`, keeping the files it already holds; a file at a path the mount
135
+ * already has is replaced, and a missing mount is created. This suits a page that brings files in a
136
+ * piece at a time, since it only sends what's new. The worker checks the whole batch first, so a
137
+ * rejection (a path that is already a directory, say) leaves the mount as it was. Buffers are
138
+ * transferred as with mountFiles.
139
+ */
140
+ export function addFiles(worker: Worker, name: string, files: Files): Promise<void> {
141
+ return sendFiles(worker, "add", name, files);
142
+ }
143
+
144
+ async function sendFiles(worker: Worker, kind: "mount" | "add", name: string, files: Files): Promise<void> {
145
+ const transfer = new Set<ArrayBuffer>();
146
+ for (const b of Object.values(files)) {
147
+ // A tag check rather than instanceof, which fails for a buffer from another realm (an iframe),
148
+ // and a SharedArrayBuffer can't be transferred.
149
+ if (Object.prototype.toString.call(b.buffer) === "[object ArrayBuffer]") transfer.add(b.buffer as ArrayBuffer);
150
+ }
151
+ const reply = await channel(worker).call({ kind, name, files }, [...transfer]);
152
+ if (!reply.ok) throw new Error(reply.error);
153
+ }
154
+
155
+ /**
156
+ * The bytes of linear memory the worker's wasm holds. Wasm memory grows and never shrinks, so this
157
+ * is the peak the engine has needed so far, which is what an app weighs before giving a browser a
158
+ * bigger job.
159
+ */
160
+ export async function workerMemory(worker: Worker): Promise<number> {
161
+ const reply = await channel(worker).call({ kind: "stats" }, []);
162
+ if (!reply.ok) throw new Error(reply.error);
163
+ return reply.memoryBytes ?? 0;
164
+ }
165
+
166
+ /** Removes the mount `name`. Removing a name that isn't mounted succeeds. */
167
+ export async function unmountFiles(worker: Worker, name: string): Promise<void> {
168
+ const reply = await channel(worker).call({ kind: "unmount", name }, []);
169
+ if (!reply.ok) throw new Error(reply.error);
170
+ }
@@ -0,0 +1,74 @@
1
+ // Turning what a user drops or picks into the {path: bytes} map mountFiles takes. The bytes are
2
+ // read here, before any request, because the Go side can't wait on File.arrayBuffer() mid-request.
3
+ import type { Files } from "./protocol";
4
+
5
+ // The parts of the File and Directory Entries API used here. Chromium, Firefox and Safari all ship
6
+ // it under the webkit prefix.
7
+ interface Entry {
8
+ isFile: boolean;
9
+ isDirectory: boolean;
10
+ fullPath: string;
11
+ }
12
+ interface FileEntry extends Entry {
13
+ file(ok: (f: File) => void, err: (e: unknown) => void): void;
14
+ }
15
+ interface DirectoryEntry extends Entry {
16
+ createReader(): { readEntries(ok: (es: Entry[]) => void, err: (e: unknown) => void): void };
17
+ }
18
+
19
+ /**
20
+ * Reads a drop into files keyed by path. A dropped folder keeps its own name as the first path
21
+ * element ("design/board.edn"), and dropped loose files sit at the top. Entries the browser can't
22
+ * read as files or folders are skipped.
23
+ */
24
+ export async function filesFromDrop(dt: DataTransfer): Promise<Files> {
25
+ const entries: Entry[] = [];
26
+ const loose: File[] = [];
27
+ for (const item of Array.from(dt.items)) {
28
+ if (item.kind !== "file") continue;
29
+ const entry = (item as DataTransferItem & { webkitGetAsEntry?: () => Entry | null }).webkitGetAsEntry?.();
30
+ if (entry) entries.push(entry);
31
+ else {
32
+ const f = item.getAsFile();
33
+ if (f) loose.push(f);
34
+ }
35
+ }
36
+ const out: Files = {};
37
+ await Promise.all([
38
+ ...entries.map((e) => readEntry(e, out)),
39
+ ...loose.map(async (f) => {
40
+ out[f.name] = new Uint8Array(await f.arrayBuffer());
41
+ }),
42
+ ]);
43
+ return out;
44
+ }
45
+
46
+ /**
47
+ * Reads an `<input type="file" webkitdirectory>` (or `multiple`) selection into files keyed by
48
+ * path. Folder picks use each file's webkitRelativePath, which starts with the folder's name.
49
+ */
50
+ export async function filesFromFileList(list: FileList | File[]): Promise<Files> {
51
+ const out: Files = {};
52
+ await Promise.all(
53
+ Array.from(list).map(async (f) => {
54
+ out[f.webkitRelativePath || f.name] = new Uint8Array(await f.arrayBuffer());
55
+ }),
56
+ );
57
+ return out;
58
+ }
59
+
60
+ async function readEntry(entry: Entry, out: Files): Promise<void> {
61
+ if (entry.isFile) {
62
+ const f = await new Promise<File>((ok, err) => (entry as FileEntry).file(ok, err));
63
+ out[entry.fullPath.replace(/^\/+/, "")] = new Uint8Array(await f.arrayBuffer());
64
+ return;
65
+ }
66
+ if (!entry.isDirectory) return;
67
+ const reader = (entry as DirectoryEntry).createReader();
68
+ // readEntries returns a batch at a time (100 in Chromium) and an empty batch at the end.
69
+ for (;;) {
70
+ const batch = await new Promise<Entry[]>((ok, err) => reader.readEntries(ok, err));
71
+ if (batch.length === 0) return;
72
+ await Promise.all(batch.map((e) => readEntry(e, out)));
73
+ }
74
+ }
@@ -0,0 +1,3 @@
1
+ export { startWorker, workerFetch, mountFiles, addFiles, unmountFiles, workerMemory } from "./client";
2
+ export type { StartWorkerOptions, Files } from "./client";
3
+ export { filesFromDrop, filesFromFileList } from "./drop";
@@ -0,0 +1,34 @@
1
+ // Messages between the page (client.ts) and the wasm host worker (worker.ts). One request message
2
+ // and one reply per call, matched by id. Byte buffers travel as transferables, not copies.
3
+
4
+ /** A mount's files, keyed by slash-separated path within the mount. */
5
+ export type Files = Record<string, Uint8Array>;
6
+
7
+ export type HostRequest =
8
+ | {
9
+ id: number;
10
+ kind: "http";
11
+ method: string;
12
+ url: string;
13
+ headers: Record<string, string>;
14
+ body: Uint8Array | null;
15
+ }
16
+ | { id: number; kind: "mount"; name: string; files: Files }
17
+ | { id: number; kind: "add"; name: string; files: Files }
18
+ | { id: number; kind: "stats" }
19
+ | { id: number; kind: "unmount"; name: string };
20
+
21
+ export type HostReply =
22
+ | {
23
+ id: number;
24
+ ok: true;
25
+ status?: number;
26
+ headers?: Record<string, string>;
27
+ body?: Uint8Array;
28
+ /** For stats: the wasm linear memory's size, which only grows. */
29
+ memoryBytes?: number;
30
+ }
31
+ | { id: number; ok: false; error: string };
32
+
33
+ /** Sent once when the wasm has loaded (or failed to), and again if the Go program exits. */
34
+ export type HostStatus = { ready: true } | { ready: false; error: string } | { exited: string };
@@ -0,0 +1,94 @@
1
+ // The wasm host worker. It loads a Go wasm build that calls goapplib's wasmhost.Serve (or
2
+ // ServeRebuild) and forwards the page's messages to the functions that build exports on
3
+ // globalThis[ns]. Running the Go program here, off the page's thread, keeps a request that takes
4
+ // seconds from freezing the page.
5
+ //
6
+ // It is a classic worker, built as an IIFE, because Go's wasm_exec.js is a plain script defining
7
+ // globalThis.Go, which importScripts can load and a module worker cannot. The asset URLs and the
8
+ // namespace come in the worker's query string (?wasm=&exec=&ns=), which startWorker writes.
9
+ import type { Files, HostReply, HostRequest, HostStatus } from "./protocol";
10
+
11
+ interface Exports {
12
+ ready?: () => void;
13
+ http(
14
+ method: string,
15
+ url: string,
16
+ headers: Record<string, string>,
17
+ body: Uint8Array | null,
18
+ ): Promise<{ status: number; headers: Record<string, string>; body: Uint8Array }>;
19
+ mount(name: string, files: Files): Promise<void>;
20
+ add(name: string, files: Files): Promise<void>;
21
+ unmount(name: string): Promise<void>;
22
+ }
23
+
24
+ // The project compiles against the DOM lib, which describes a window, and the WebWorker lib can't
25
+ // be loaded beside it, so the few worker globals used here are declared.
26
+ declare function importScripts(...urls: string[]): void;
27
+ declare const self: {
28
+ location: Location;
29
+ postMessage(message: unknown, transfer?: Transferable[]): void;
30
+ onmessage: ((ev: MessageEvent<HostRequest>) => void) | null;
31
+ Go: new () => { importObject: WebAssembly.Imports; run(i: WebAssembly.Instance): Promise<void> };
32
+ } & Record<string, unknown>;
33
+
34
+ const params = new URLSearchParams(self.location.search);
35
+ const wasmUrl = params.get("wasm") ?? "app.wasm";
36
+ const execUrl = params.get("exec") ?? "wasm_exec.js";
37
+ const ns = params.get("ns") ?? "wasmhost";
38
+
39
+ // The wasm's linear memory, exported by Go as `mem`. It only grows, so its size is the peak so far.
40
+ let memory: WebAssembly.Memory | undefined;
41
+
42
+ const post = (m: HostStatus | HostReply, transfer: Transferable[] = []) => self.postMessage(m, transfer);
43
+
44
+ async function boot(): Promise<Exports> {
45
+ importScripts(execUrl);
46
+ const go = new self.Go();
47
+ const ready = new Promise<void>((resolve) => {
48
+ self[ns] = { ready: resolve };
49
+ });
50
+ const res = await fetch(wasmUrl);
51
+ if (!res.ok) throw new Error(`fetch ${wasmUrl}: ${res.status}`);
52
+ // instantiateStreaming insists on the application/wasm content type, which not every static
53
+ // server sends.
54
+ const { instance } = res.headers.get("content-type")?.startsWith("application/wasm")
55
+ ? await WebAssembly.instantiateStreaming(res, go.importObject)
56
+ : await WebAssembly.instantiate(await res.arrayBuffer(), go.importObject);
57
+ memory = instance.exports.mem as WebAssembly.Memory | undefined;
58
+ void go.run(instance).then(() => post({ exited: "the Go program exited" }));
59
+ await ready;
60
+ return self[ns] as Exports;
61
+ }
62
+
63
+ const booted = boot();
64
+ booted.then(
65
+ () => post({ ready: true }),
66
+ (err: unknown) => post({ ready: false, error: String(err) }),
67
+ );
68
+
69
+ self.onmessage = async (ev) => {
70
+ const req = ev.data;
71
+ try {
72
+ const host = await booted;
73
+ if (req.kind === "http") {
74
+ const res = await host.http(req.method, req.url, req.headers, req.body);
75
+ post({ id: req.id, ok: true, status: res.status, headers: res.headers, body: res.body }, [res.body.buffer]);
76
+ } else if (req.kind === "mount") {
77
+ await host.mount(req.name, req.files);
78
+ post({ id: req.id, ok: true });
79
+ } else if (req.kind === "add") {
80
+ await host.add(req.name, req.files);
81
+ post({ id: req.id, ok: true });
82
+ } else if (req.kind === "stats") {
83
+ post({ id: req.id, ok: true, memoryBytes: memory?.buffer.byteLength ?? 0 });
84
+ } else if (req.kind === "unmount") {
85
+ await host.unmount(req.name);
86
+ post({ id: req.id, ok: true });
87
+ } else {
88
+ // A page newer than this worker can send a kind it doesn't know; say so rather than guess.
89
+ throw new Error(`unknown request kind ${JSON.stringify((req as { kind: unknown }).kind)}`);
90
+ }
91
+ } catch (err) {
92
+ post({ id: req.id, ok: false, error: err instanceof Error ? err.message : String(err) });
93
+ }
94
+ };