@panyam/tsappkit 0.2.0 → 0.6.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.6.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
  }
package/src/index.ts CHANGED
@@ -31,8 +31,10 @@ export { MobileBottomDrawer } from './MobileBottomDrawer';
31
31
  export { IslandPage } from './page/IslandPage';
32
32
  export { readSpec, SPEC_ELEMENT_ID } from './page/spec';
33
33
  export type { IslandSpec, PageSpec, SpecExtension } from './page/spec';
34
- export { mountIslands } from './page/mount';
35
- export type { IslandFactory, Registry } from './page/mount';
34
+ export { lazy, mountIslands } from './page/mount';
35
+ export type { IslandFactory, LazyIsland, MountOptions, Registry } from './page/mount';
36
+ export { parseLoad, scheduleMount } from './page/load';
37
+ export type { LoadEnv, LoadStrategy } from './page/load';
36
38
 
37
39
  // Utilities
38
40
  export { isInInputContext, hasModifierKeys, shouldIgnoreShortcut } from './DOMUtils';
@@ -1,6 +1,8 @@
1
1
  import { BasePage } from "../BasePage";
2
2
  import type { EventBus } from "../EventBus";
3
3
  import type { LCMComponent } from "../LCMComponent";
4
+ import { LifecycleController } from "../LifecycleController";
5
+ import { parseLoad, scheduleMount, type LoadStrategy } from "./load";
4
6
  import { mountIslands, type Registry } from "./mount";
5
7
  import { readSpec, SPEC_ELEMENT_ID, type PageSpec } from "./spec";
6
8
 
@@ -15,6 +17,13 @@ import { readSpec, SPEC_ELEMENT_ID, type PageSpec } from "./spec";
15
17
  * fields in `readExtension`, and they arrive typed as `Ext` on the spec
16
18
  * `makeContext` gets.
17
19
  *
20
+ * Each island mounts when its `load` says (`eager`, `idle`, `visible`,
21
+ * `media:<query>`; see scheduleMount), and a `lazy` registry entry loads its
22
+ * chunk then. One that mounts later, a lazy eager one included, goes through
23
+ * its own LifecycleController, so it still gets performLocalInit,
24
+ * setupDependencies and activate. A deferred island mustn't be something
25
+ * another island or the page needs at startup: nothing waits for it.
26
+ *
18
27
  * A page with no readable `#page-spec` mounts nothing and warns. Subclasses
19
28
  * that override initializeSpecificComponents call super and add to what it
20
29
  * returns.
@@ -44,6 +53,22 @@ export abstract class IslandPage<Ctx, Ext extends object = {}> extends BasePage
44
53
  () => this.makeContext(spec),
45
54
  this.eventBus,
46
55
  (message) => console.warn(message),
56
+ {
57
+ defer: (island, el, mount) => this.scheduleLoad(parseLoad(island.load) ?? { kind: "eager" }, el, mount),
58
+ onLateMount: (component, island) => {
59
+ new LifecycleController(this.eventBus, LifecycleController.DefaultConfig)
60
+ .initializeFromRoot(component)
61
+ .catch((err) => console.warn(`page spec: island "${island.name}" failed to start: ${err instanceof Error ? err.message : String(err)}`));
62
+ },
63
+ },
47
64
  );
48
65
  }
66
+
67
+ /**
68
+ * Waits for `strategy` and then calls `mount`. Defaults to scheduleMount
69
+ * against the window; a subclass or test can replace how the waiting is done.
70
+ */
71
+ protected scheduleLoad(strategy: LoadStrategy, el: HTMLElement, mount: () => void): void {
72
+ scheduleMount(strategy, el, mount);
73
+ }
49
74
  }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * When an island mounts: its `load` in the page spec (goapplib's
3
+ * page.Island.Load), read and waited for. Every strategy mounts once; an
4
+ * island isn't unmounted when its media query stops matching.
5
+ */
6
+
7
+ /** A parsed `load`. An empty or missing one is eager. */
8
+ export type LoadStrategy =
9
+ | { kind: "eager" }
10
+ | { kind: "idle" }
11
+ | { kind: "visible" }
12
+ | { kind: "media"; query: string };
13
+
14
+ /**
15
+ * The strategy `load` names, or null when it isn't one of the forms Go's
16
+ * Validate accepts (`eager`, `idle`, `visible`, `media:<query>`), compared
17
+ * exactly.
18
+ */
19
+ export function parseLoad(load: string | undefined): LoadStrategy | null {
20
+ switch (load ?? "") {
21
+ case "":
22
+ case "eager":
23
+ return { kind: "eager" };
24
+ case "idle":
25
+ return { kind: "idle" };
26
+ case "visible":
27
+ return { kind: "visible" };
28
+ }
29
+ const query = load!.startsWith("media:") ? load!.slice("media:".length) : "";
30
+ return query.trim() ? { kind: "media", query } : null;
31
+ }
32
+
33
+ /**
34
+ * The browser APIs scheduleMount waits on, so tests can stand them in.
35
+ * `requestIdleCallback` is optional because Safari doesn't have it.
36
+ */
37
+ export interface LoadEnv {
38
+ requestIdleCallback?: (cb: () => void) => unknown;
39
+ setTimeout: (cb: () => void, ms?: number) => unknown;
40
+ IntersectionObserver: typeof IntersectionObserver;
41
+ matchMedia: (query: string) => MediaQueryList;
42
+ }
43
+
44
+ /**
45
+ * Calls `mount` once, when `strategy` says the island in `el` should mount:
46
+ * at once for eager; when the browser is idle (or on the next task, without
47
+ * requestIdleCallback) for idle; the first time any of `el` enters the
48
+ * viewport for visible; and at once if the query matches, or else on the
49
+ * first change that makes it match, for media.
50
+ */
51
+ export function scheduleMount(strategy: LoadStrategy, el: Element, mount: () => void, env: LoadEnv = window): void {
52
+ switch (strategy.kind) {
53
+ case "eager":
54
+ mount();
55
+ return;
56
+ case "idle":
57
+ if (env.requestIdleCallback) env.requestIdleCallback(mount);
58
+ else env.setTimeout(mount, 1);
59
+ return;
60
+ case "visible": {
61
+ let done = false;
62
+ const observer = new env.IntersectionObserver((entries) => {
63
+ if (done || !entries.some((e) => e.isIntersecting)) return;
64
+ done = true;
65
+ observer.disconnect();
66
+ mount();
67
+ });
68
+ observer.observe(el);
69
+ return;
70
+ }
71
+ case "media": {
72
+ const mq = env.matchMedia(strategy.query);
73
+ if (mq.matches) {
74
+ mount();
75
+ return;
76
+ }
77
+ const onChange = (e: { matches: boolean }) => {
78
+ if (!e.matches) return;
79
+ mq.removeEventListener("change", onChange);
80
+ mount();
81
+ };
82
+ mq.addEventListener("change", onChange);
83
+ return;
84
+ }
85
+ }
86
+ }