@panyam/tsappkit 0.2.0 → 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.
@@ -30,6 +30,20 @@ declare function workerFetch(worker: Worker): (input: RequestInfo | URL, init?:
30
30
  * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).
31
31
  */
32
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>;
33
47
  /** Removes the mount `name`. Removing a name that isn't mounted succeeds. */
34
48
  declare function unmountFiles(worker: Worker, name: string): Promise<void>;
35
49
 
@@ -45,4 +59,4 @@ declare function filesFromDrop(dt: DataTransfer): Promise<Files>;
45
59
  */
46
60
  declare function filesFromFileList(list: FileList | File[]): Promise<Files>;
47
61
 
48
- export { type Files, type StartWorkerOptions, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch };
62
+ export { type Files, type StartWorkerOptions, addFiles, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch, workerMemory };
@@ -30,6 +30,20 @@ declare function workerFetch(worker: Worker): (input: RequestInfo | URL, init?:
30
30
  * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).
31
31
  */
32
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>;
33
47
  /** Removes the mount `name`. Removing a name that isn't mounted succeeds. */
34
48
  declare function unmountFiles(worker: Worker, name: string): Promise<void>;
35
49
 
@@ -45,4 +59,4 @@ declare function filesFromDrop(dt: DataTransfer): Promise<Files>;
45
59
  */
46
60
  declare function filesFromFileList(list: FileList | File[]): Promise<Files>;
47
61
 
48
- export { type Files, type StartWorkerOptions, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch };
62
+ export { type Files, type StartWorkerOptions, addFiles, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch, workerMemory };
@@ -83,13 +83,24 @@ function workerFetch(worker) {
83
83
  return new Response(resBody, { status, headers: reply.headers });
84
84
  };
85
85
  }
86
- async function mountFiles(worker, name, files) {
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) {
87
93
  const transfer = /* @__PURE__ */ new Set();
88
94
  for (const b of Object.values(files)) {
89
95
  if (Object.prototype.toString.call(b.buffer) === "[object ArrayBuffer]") transfer.add(b.buffer);
90
96
  }
91
- const reply = await channel(worker).call({ kind: "mount", name, files }, [...transfer]);
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" }, []);
92
102
  if (!reply.ok) throw new Error(reply.error);
103
+ return reply.memoryBytes ?? 0;
93
104
  }
94
105
  async function unmountFiles(worker, name) {
95
106
  const reply = await channel(worker).call({ kind: "unmount", name }, []);
@@ -142,11 +153,13 @@ async function readEntry(entry, out) {
142
153
  }
143
154
  }
144
155
 
156
+ exports.addFiles = addFiles;
145
157
  exports.filesFromDrop = filesFromDrop;
146
158
  exports.filesFromFileList = filesFromFileList;
147
159
  exports.mountFiles = mountFiles;
148
160
  exports.startWorker = startWorker;
149
161
  exports.unmountFiles = unmountFiles;
150
162
  exports.workerFetch = workerFetch;
163
+ exports.workerMemory = workerMemory;
151
164
  //# sourceMappingURL=index.js.map
152
165
  //# sourceMappingURL=index.js.map
@@ -1 +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 async function mountFiles(worker: Worker, 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: \"mount\", name, files }, [...transfer]);\n if (!reply.ok) throw new Error(reply.error);\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"]}
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"]}
@@ -81,13 +81,24 @@ function workerFetch(worker) {
81
81
  return new Response(resBody, { status, headers: reply.headers });
82
82
  };
83
83
  }
84
- async function mountFiles(worker, name, files) {
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) {
85
91
  const transfer = /* @__PURE__ */ new Set();
86
92
  for (const b of Object.values(files)) {
87
93
  if (Object.prototype.toString.call(b.buffer) === "[object ArrayBuffer]") transfer.add(b.buffer);
88
94
  }
89
- const reply = await channel(worker).call({ kind: "mount", name, files }, [...transfer]);
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" }, []);
90
100
  if (!reply.ok) throw new Error(reply.error);
101
+ return reply.memoryBytes ?? 0;
91
102
  }
92
103
  async function unmountFiles(worker, name) {
93
104
  const reply = await channel(worker).call({ kind: "unmount", name }, []);
@@ -140,6 +151,6 @@ async function readEntry(entry, out) {
140
151
  }
141
152
  }
142
153
 
143
- export { filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch };
154
+ export { addFiles, filesFromDrop, filesFromFileList, mountFiles, startWorker, unmountFiles, workerFetch, workerMemory };
144
155
  //# sourceMappingURL=index.mjs.map
145
156
  //# sourceMappingURL=index.mjs.map
@@ -1 +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 async function mountFiles(worker: Worker, 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: \"mount\", name, files }, [...transfer]);\n if (!reply.ok) throw new Error(reply.error);\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"]}
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"]}
@@ -5,6 +5,7 @@
5
5
  var wasmUrl = params.get("wasm") ?? "app.wasm";
6
6
  var execUrl = params.get("exec") ?? "wasm_exec.js";
7
7
  var ns = params.get("ns") ?? "wasmhost";
8
+ var memory;
8
9
  var post = (m, transfer = []) => self.postMessage(m, transfer);
9
10
  async function boot() {
10
11
  importScripts(execUrl);
@@ -15,6 +16,7 @@
15
16
  const res = await fetch(wasmUrl);
16
17
  if (!res.ok) throw new Error(`fetch ${wasmUrl}: ${res.status}`);
17
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;
18
20
  void go.run(instance).then(() => post({ exited: "the Go program exited" }));
19
21
  await ready;
20
22
  return self[ns];
@@ -34,9 +36,16 @@
34
36
  } else if (req.kind === "mount") {
35
37
  await host.mount(req.name, req.files);
36
38
  post({ id: req.id, ok: true });
37
- } else {
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") {
38
45
  await host.unmount(req.name);
39
46
  post({ id: req.id, ok: true });
47
+ } else {
48
+ throw new Error(`unknown request kind ${JSON.stringify(req.kind)}`);
40
49
  }
41
50
  } catch (err) {
42
51
  post({ id: req.id, ok: false, error: err instanceof Error ? err.message : String(err) });
@@ -1 +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 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\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 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 {\n await host.unmount(req.name);\n post({ id: req.id, ok: true });\n }\n } catch (err) {\n post({ id: req.id, ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n};\n"],"mappings":";;;AAgCA,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;AAE/B,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,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,OAAO;AACL,cAAM,KAAK,QAAQ,IAAI,IAAI;AAC3B,aAAK,EAAE,IAAI,IAAI,IAAI,IAAI,KAAK,CAAC;AAAA,MAC/B;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":[]}
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.2.0",
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",
@@ -71,6 +71,7 @@
71
71
  },
72
72
  "repository": {
73
73
  "type": "git",
74
- "url": ""
74
+ "url": "git+https://github.com/panyam/goapplib.git",
75
+ "directory": "tsappkit"
75
76
  }
76
77
  }
@@ -126,15 +126,41 @@ export function workerFetch(worker: Worker): (input: RequestInfo | URL, init?: R
126
126
  * each Uint8Array is empty here afterwards; copy first anything the page still needs. Resolves once
127
127
  * the worker has mounted them (and, for a rebuild-mode host, rebuilt its handler).
128
128
  */
129
- export async function mountFiles(worker: Worker, name: string, files: Files): Promise<void> {
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> {
130
145
  const transfer = new Set<ArrayBuffer>();
131
146
  for (const b of Object.values(files)) {
132
147
  // A tag check rather than instanceof, which fails for a buffer from another realm (an iframe),
133
148
  // and a SharedArrayBuffer can't be transferred.
134
149
  if (Object.prototype.toString.call(b.buffer) === "[object ArrayBuffer]") transfer.add(b.buffer as ArrayBuffer);
135
150
  }
136
- const reply = await channel(worker).call({ kind: "mount", name, files }, [...transfer]);
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" }, []);
137
162
  if (!reply.ok) throw new Error(reply.error);
163
+ return reply.memoryBytes ?? 0;
138
164
  }
139
165
 
140
166
  /** Removes the mount `name`. Removing a name that isn't mounted succeeds. */
@@ -1,3 +1,3 @@
1
- export { startWorker, workerFetch, mountFiles, unmountFiles } from "./client";
1
+ export { startWorker, workerFetch, mountFiles, addFiles, unmountFiles, workerMemory } from "./client";
2
2
  export type { StartWorkerOptions, Files } from "./client";
3
3
  export { filesFromDrop, filesFromFileList } from "./drop";
@@ -14,10 +14,20 @@ export type HostRequest =
14
14
  body: Uint8Array | null;
15
15
  }
16
16
  | { id: number; kind: "mount"; name: string; files: Files }
17
+ | { id: number; kind: "add"; name: string; files: Files }
18
+ | { id: number; kind: "stats" }
17
19
  | { id: number; kind: "unmount"; name: string };
18
20
 
19
21
  export type HostReply =
20
- | { id: number; ok: true; status?: number; headers?: Record<string, string>; body?: Uint8Array }
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
+ }
21
31
  | { id: number; ok: false; error: string };
22
32
 
23
33
  /** Sent once when the wasm has loaded (or failed to), and again if the Go program exits. */
@@ -17,6 +17,7 @@ interface Exports {
17
17
  body: Uint8Array | null,
18
18
  ): Promise<{ status: number; headers: Record<string, string>; body: Uint8Array }>;
19
19
  mount(name: string, files: Files): Promise<void>;
20
+ add(name: string, files: Files): Promise<void>;
20
21
  unmount(name: string): Promise<void>;
21
22
  }
22
23
 
@@ -35,6 +36,9 @@ const wasmUrl = params.get("wasm") ?? "app.wasm";
35
36
  const execUrl = params.get("exec") ?? "wasm_exec.js";
36
37
  const ns = params.get("ns") ?? "wasmhost";
37
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
+
38
42
  const post = (m: HostStatus | HostReply, transfer: Transferable[] = []) => self.postMessage(m, transfer);
39
43
 
40
44
  async function boot(): Promise<Exports> {
@@ -50,6 +54,7 @@ async function boot(): Promise<Exports> {
50
54
  const { instance } = res.headers.get("content-type")?.startsWith("application/wasm")
51
55
  ? await WebAssembly.instantiateStreaming(res, go.importObject)
52
56
  : await WebAssembly.instantiate(await res.arrayBuffer(), go.importObject);
57
+ memory = instance.exports.mem as WebAssembly.Memory | undefined;
53
58
  void go.run(instance).then(() => post({ exited: "the Go program exited" }));
54
59
  await ready;
55
60
  return self[ns] as Exports;
@@ -71,9 +76,17 @@ self.onmessage = async (ev) => {
71
76
  } else if (req.kind === "mount") {
72
77
  await host.mount(req.name, req.files);
73
78
  post({ id: req.id, ok: true });
74
- } else {
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") {
75
85
  await host.unmount(req.name);
76
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)}`);
77
90
  }
78
91
  } catch (err) {
79
92
  post({ id: req.id, ok: false, error: err instanceof Error ? err.message : String(err) });