bito-code-review[bot] commented on code in PR #44608: URL: https://github.com/apache/superset/pull/44608#discussion_r4098600225
########## superset-embedded-sdk/testrig/host.html: ########## @@ -0,0 +1,208 @@ +<!doctype html> +<html lang="en"> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with this + work for additional information regarding copyright ownership. The ASF + licenses this file to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +--> +<head> + <meta charset="utf-8" /> + <title>embedded-sdk test rig — host app</title> + <style> + body { margin: 0; font: 14px/1.5 system-ui, sans-serif; display: grid; + grid-template-columns: 1fr 420px; height: 100vh; } + main { padding: 16px; overflow: auto; } + aside { border-left: 1px solid #ddd; padding: 16px; overflow: auto; background: #fafafa; } + h1 { font-size: 16px; margin: 0 0 12px; } + #mount { border: 1px solid #ccc; border-radius: 8px; height: 320px; } + #mount iframe { width: 100%; height: 100%; border: 0; border-radius: 8px; } + button { font: inherit; margin: 0 6px 6px 0; padding: 4px 10px; } + #log { font: 12px/1.45 ui-monospace, monospace; white-space: pre-wrap; } + .t { color: #888; } + .err { color: #b00; } + .ok { color: #070; } + </style> +</head> +<body> + <main> + <h1>Host app</h1> + <div> + <button id="embed">embed</button> + <button id="theme">setThemeMode(dark) + setThemeConfig</button> + <button id="tabs">getActiveTabs()</button> + <button id="unmount">unmount</button> + <button id="clear">clear log</button> + </div> + <div id="mount"></div> + <p class="t">Tokens minted by this host's endpoint: <b id="minted">0</b></p> + </main> + <aside><div id="log"></div></aside> + +<script src="/sdk.js"></script> +<script> + const SUPERSET_ORIGIN = "__SUPERSET_ORIGIN__"; + + // Everything the driver reads. + const rig = (window.rig = { + events: [], // what the embedded page reported over its side channel + log: [], // what the host app saw + errors: [], // rejections the SDK handed back + tokenFetches: 0, // calls the SDK made to fetchGuestToken + dashboard: null, + embedState: "idle", // pending | resolved | rejected + embedError: null, + heldFirstFetch: null, + }); + + function log(kind, ...parts) { + const line = `${new Date().toISOString().slice(11, 23)} ${kind} ${parts + .map((p) => (typeof p === "string" ? p : JSON.stringify(p))) + .join(" ")}`; + rig.log.push(line); + const el = document.createElement("div"); + el.className = kind === "error" ? "err" : kind === "ok" ? "ok" : ""; + el.textContent = line; + document.getElementById("log").prepend(el); + } + + // The embedded page's side channel (not the SDK's MessageChannel). + window.addEventListener("message", (event) => { + if (!event.data?.__rig) return; + rig.events.push({ ...event.data, at: Date.now() }); + log("guest", `page ${event.data.page}`, event.data.event, event.data.detail ?? ""); + }); + + async function refreshStats() { + const stats = await fetch("/stats").then((r) => r.json()); + document.getElementById("minted").textContent = stats.tokensMinted; + rig.tokensMinted = stats.tokensMinted; + return stats; + } + window.rigRefreshStats = refreshStats; + + // TTL is a query param so a run can exercise the refresh timer in seconds + // rather than minutes. + const search = new URLSearchParams(location.search); + const ttl = search.get("ttl") || "300"; + // `?slowfirst=1` holds the very first fetchGuestToken() until the driver + // settles it by hand, which is the only way to have a navigation happen + // while the initial token is still in flight. `?failnth=N` makes call N + // reject, standing in for a host endpoint that is down. + const slowFirst = search.get("slowfirst"); + const failNth = Number(search.get("failnth") || 0); + + async function mintToken() { + const { token } = await fetch(`/guest-token?ttl=${ttl}`).then((r) => r.json()); + refreshStats(); + return token; + } + + async function fetchGuestToken() { + rig.tokenFetches += 1; + const call = rig.tokenFetches; + log("host", `fetchGuestToken() #${call}`); + if (call === failNth) { + log("error", `fetchGuestToken() #${call} rejects (rig)`); + throw new Error(`rig: host token endpoint is down (call #${call})`); + } + if (slowFirst && call === 1) { + log("host", "fetchGuestToken() #1 held by the rig"); + return new Promise((resolve, reject) => { + rig.heldFirstFetch = { resolve, reject }; + }); + } + return mintToken(); + } + + // Settle the held first fetch, long after the SDK asked for it. + window.rigReleaseFirstToken = async () => { + const token = await mintToken(); + rig.heldFirstFetch.resolve(token); + log("host", "released the held fetchGuestToken() #1"); + }; + window.rigFailFirstToken = () => { + rig.heldFirstFetch.reject(new Error("rig: host token endpoint is down")); + log("error", "failed the held fetchGuestToken() #1"); + }; + + document.getElementById("embed").onclick = () => { + // Deliberately not awaited: a run needs to watch what happens while + // `embedDashboard` is still pending, and to see how it ends up settling. + rig.embedState = "pending"; + supersetEmbeddedSdk + .embedDashboard({ + id: "rig-dashboard", + supersetDomain: SUPERSET_ORIGIN, + mountPoint: document.getElementById("mount"), + fetchGuestToken, + debug: true, + dashboardUiConfig: search.get("hang") + ? { urlParams: { hang: "1" } } + : undefined, + }) + .then( + (dashboard) => { + rig.dashboard = dashboard; + rig.embedState = "resolved"; + log("ok", "embedDashboard resolved"); + }, + (err) => { + rig.embedState = "rejected"; + rig.embedError = { name: err.name, message: err.message }; + log("error", `embedDashboard rejected: ${err.message}`); + }, + ); + }; + + document.getElementById("theme").onclick = () => { + rig.dashboard.setThemeConfig({ token: { colorPrimary: "#ff0066" } }); + rig.dashboard.setThemeMode("dark"); + log("host", "theme pushed"); + }; Review Comment: <!-- Bito Reply --> The suggestion is appropriate and improves the code by preventing potential TypeErrors when the dashboard is not yet initialized. Applying this guard ensures consistency with other parts of the driver script that already implement this check. **superset-embedded-sdk/testrig/host.html** ``` document.getElementById("theme").onclick = () => { if (!rig.dashboard) { log("error", "theme: no dashboard yet"); return; } rig.dashboard.setThemeConfig({ token: { colorPrimary: "#ff0066" } }); rig.dashboard.setThemeMode("dark"); log("host", "theme pushed"); }; ``` ########## superset-embedded-sdk/testrig/drive.mjs: ########## @@ -0,0 +1,573 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Drives the rig in headless Chromium over the DevTools protocol, with no +// dependencies beyond a chromium binary. `node drive.mjs [--headed] [--verbose]` + +import { spawn, execFileSync } from "node:child_process"; +import { mkdtempSync, existsSync, statSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { start, HOST_PORT } from "./server.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const sdkRoot = join(here, ".."); +const headed = process.argv.includes("--headed"); +const verbose = process.argv.includes("--verbose"); + +const CHROMIUM = + process.env.CHROMIUM_PATH || + ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"].find( + (bin) => { + try { + execFileSync("which", [bin], { stdio: "ignore" }); + return true; + } catch { + return false; + } + }, + ); + +// ------------------------------------------------------------------ results +const results = []; +function check(name, ok, detail = "") { + results.push({ name, ok, detail }); + const mark = ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"; + console.log(` ${mark} ${name}${detail && !ok ? `\n ${detail}` : ""}`); +} + +// Waits for something to become true, and records the wait itself as a check, +// so a run against code that never gets there reports a failure per scenario +// instead of stopping at the first one. +async function expect(name, predicate, timeoutMs = 15_000) { + try { + await waitFor(predicate, name, timeoutMs); + check(name, true); + return true; + } catch (err) { + check(name, false, err.message); + return false; + } +} + +// ---------------------------------------------------------------- cdp client +class CDP { + constructor(ws) { + this.ws = ws; + this.nextId = 0; + this.pending = new Map(); + this.listeners = []; + ws.addEventListener("message", (event) => { + const msg = JSON.parse(event.data); Review Comment: <!-- Bito Reply --> The suggestion to wrap `JSON.parse(event.data)` in a try/catch block is correct and highly recommended. In the current implementation, an unhandled exception during parsing would propagate to the event listener, potentially causing an uncaught exception that terminates the process. Applying this guard ensures the rig remains stable even if it receives malformed or non-JSON frames. **superset-embedded-sdk/testrig/drive.mjs** ``` ws.addEventListener("message", (event) => { try { const msg = JSON.parse(event.data); // ... handle message } catch (e) { console.error("Failed to parse CDP message:", e); } ``` ########## superset-embedded-sdk/testrig/drive.mjs: ########## @@ -0,0 +1,573 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Drives the rig in headless Chromium over the DevTools protocol, with no +// dependencies beyond a chromium binary. `node drive.mjs [--headed] [--verbose]` + +import { spawn, execFileSync } from "node:child_process"; +import { mkdtempSync, existsSync, statSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { start, HOST_PORT } from "./server.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const sdkRoot = join(here, ".."); +const headed = process.argv.includes("--headed"); +const verbose = process.argv.includes("--verbose"); + +const CHROMIUM = + process.env.CHROMIUM_PATH || + ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"].find( + (bin) => { + try { + execFileSync("which", [bin], { stdio: "ignore" }); + return true; + } catch { + return false; + } + }, + ); + +// ------------------------------------------------------------------ results +const results = []; +function check(name, ok, detail = "") { + results.push({ name, ok, detail }); + const mark = ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"; + console.log(` ${mark} ${name}${detail && !ok ? `\n ${detail}` : ""}`); +} + +// Waits for something to become true, and records the wait itself as a check, +// so a run against code that never gets there reports a failure per scenario +// instead of stopping at the first one. +async function expect(name, predicate, timeoutMs = 15_000) { + try { + await waitFor(predicate, name, timeoutMs); + check(name, true); + return true; + } catch (err) { + check(name, false, err.message); + return false; + } +} + +// ---------------------------------------------------------------- cdp client +class CDP { + constructor(ws) { + this.ws = ws; + this.nextId = 0; + this.pending = new Map(); + this.listeners = []; + ws.addEventListener("message", (event) => { + const msg = JSON.parse(event.data); + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id); + this.pending.delete(msg.id); + if (msg.error) reject(new Error(JSON.stringify(msg.error))); + else resolve(msg.result); + } else { + this.listeners.forEach((fn) => fn(msg)); + } + }); + } + + send(method, params = {}, sessionId) { + const id = (this.nextId += 1); + const payload = { id, method, params }; + if (sessionId) payload.sessionId = sessionId; + this.ws.send(JSON.stringify(payload)); + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + setTimeout(() => { + if (this.pending.delete(id)) reject(new Error(`${method} timed out`)); + }, 30_000); + }); + } +} + +async function launchBrowser() { + const userDataDir = mkdtempSync(join(tmpdir(), "embedded-sdk-rig-")); + const args = [ + ...(headed ? [] : ["--headless=new"]), + "--remote-debugging-port=0", + `--user-data-dir=${userDataDir}`, + "--no-first-run", + "--no-default-browser-check", + "--disable-gpu", + "--disable-dev-shm-usage", + "about:blank", + ]; + const child = spawn(CHROMIUM, args, { stdio: ["ignore", "pipe", "pipe"] }); + const wsUrl = await new Promise((resolve, reject) => { + let buffered = ""; + const onChunk = (chunk) => { + buffered += chunk; + const match = buffered.match(/ws:\/\/\S+/); + if (match) resolve(match[0]); + }; + child.stdout.on("data", onChunk); + child.stderr.on("data", onChunk); + child.on("exit", (code) => + reject(new Error(`chromium exited (${code}) before listening:\n${buffered}`)), + ); + setTimeout(() => reject(new Error("chromium never reported a devtools url")), 20_000); + }); + const ws = new WebSocket(wsUrl); + await new Promise((resolve, reject) => { + ws.addEventListener("open", resolve, { once: true }); + ws.addEventListener("error", reject, { once: true }); + }); Review Comment: <!-- Bito Reply --> The suggestion to wrap the post-spawn logic in a try/catch block is correct. Currently, if the WebSocket handshake fails, the error propagates out of `launchBrowser` before the cleanup logic in `main` can execute, leaving the Chromium process orphaned and the temporary directory unremoved. Applying this change ensures that the child process is killed and the directory is cleaned up regardless of whether the handshake succeeds or fails. **superset-embedded-sdk/testrig/drive.mjs** ``` const ws = new WebSocket(wsUrl); try { await new Promise((resolve, reject) => { ws.addEventListener("open", resolve, { once: true }); ws.addEventListener("error", reject, { once: true }); }); } catch (err) { child.kill(); rmSync(userDataDir, { recursive: true, force: true }); throw err; } ``` ########## superset-embedded-sdk/testrig/drive.mjs: ########## @@ -0,0 +1,573 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Drives the rig in headless Chromium over the DevTools protocol, with no +// dependencies beyond a chromium binary. `node drive.mjs [--headed] [--verbose]` + +import { spawn, execFileSync } from "node:child_process"; +import { mkdtempSync, existsSync, statSync, readdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { start, HOST_PORT } from "./server.mjs"; + +const here = dirname(fileURLToPath(import.meta.url)); +const sdkRoot = join(here, ".."); +const headed = process.argv.includes("--headed"); +const verbose = process.argv.includes("--verbose"); + +const CHROMIUM = + process.env.CHROMIUM_PATH || + ["chromium", "chromium-browser", "google-chrome", "google-chrome-stable"].find( + (bin) => { + try { + execFileSync("which", [bin], { stdio: "ignore" }); + return true; + } catch { + return false; + } + }, + ); + +// ------------------------------------------------------------------ results +const results = []; +function check(name, ok, detail = "") { + results.push({ name, ok, detail }); + const mark = ok ? "\x1b[32m✓\x1b[0m" : "\x1b[31m✗\x1b[0m"; + console.log(` ${mark} ${name}${detail && !ok ? `\n ${detail}` : ""}`); +} + +// Waits for something to become true, and records the wait itself as a check, +// so a run against code that never gets there reports a failure per scenario +// instead of stopping at the first one. +async function expect(name, predicate, timeoutMs = 15_000) { + try { + await waitFor(predicate, name, timeoutMs); + check(name, true); + return true; + } catch (err) { + check(name, false, err.message); + return false; + } +} + +// ---------------------------------------------------------------- cdp client +class CDP { + constructor(ws) { + this.ws = ws; + this.nextId = 0; + this.pending = new Map(); + this.listeners = []; + ws.addEventListener("message", (event) => { + const msg = JSON.parse(event.data); + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id); + this.pending.delete(msg.id); + if (msg.error) reject(new Error(JSON.stringify(msg.error))); + else resolve(msg.result); + } else { + this.listeners.forEach((fn) => fn(msg)); + } + }); + } + + send(method, params = {}, sessionId) { + const id = (this.nextId += 1); + const payload = { id, method, params }; + if (sessionId) payload.sessionId = sessionId; + this.ws.send(JSON.stringify(payload)); + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + setTimeout(() => { + if (this.pending.delete(id)) reject(new Error(`${method} timed out`)); + }, 30_000); + }); + } +} + +async function launchBrowser() { + const userDataDir = mkdtempSync(join(tmpdir(), "embedded-sdk-rig-")); + const args = [ + ...(headed ? [] : ["--headless=new"]), + "--remote-debugging-port=0", + `--user-data-dir=${userDataDir}`, + "--no-first-run", + "--no-default-browser-check", + "--disable-gpu", + "--disable-dev-shm-usage", + "about:blank", + ]; + const child = spawn(CHROMIUM, args, { stdio: ["ignore", "pipe", "pipe"] }); + const wsUrl = await new Promise((resolve, reject) => { + let buffered = ""; + const onChunk = (chunk) => { + buffered += chunk; + const match = buffered.match(/ws:\/\/\S+/); + if (match) resolve(match[0]); + }; + child.stdout.on("data", onChunk); + child.stderr.on("data", onChunk); + child.on("exit", (code) => + reject(new Error(`chromium exited (${code}) before listening:\n${buffered}`)), + ); + setTimeout(() => reject(new Error("chromium never reported a devtools url")), 20_000); + }); + const ws = new WebSocket(wsUrl); + await new Promise((resolve, reject) => { + ws.addEventListener("open", resolve, { once: true }); + ws.addEventListener("error", reject, { once: true }); + }); + return { + cdp: new CDP(ws), + stop: () => { + try { + child.kill("SIGKILL"); + } finally { + rmSync(userDataDir, { recursive: true, force: true }); + } + }, + }; +} + +// ------------------------------------------------------------------- a page +async function openPage(cdp, url) { + const { targetId } = await cdp.send("Target.createTarget", { url: "about:blank" }); + const { sessionId } = await cdp.send("Target.attachToTarget", { + targetId, + flatten: true, + }); + await cdp.send("Runtime.enable", {}, sessionId); + await cdp.send("Page.enable", {}, sessionId); + if (verbose) { + cdp.listeners.push((msg) => { + if (msg.method === "Runtime.consoleAPICalled" && msg.sessionId === sessionId) { + const text = msg.params.args + .map((a) => a.value ?? a.description ?? a.type) + .join(" "); + console.log(` [page] ${text}`); + } + }); + } + + const evaluate = async (expression) => { + const r = await cdp.send( + "Runtime.evaluate", + { expression, awaitPromise: true, returnByValue: true }, + sessionId, + ); + if (r.exceptionDetails) { + throw new Error( + r.exceptionDetails.exception?.description || + r.exceptionDetails.text || + JSON.stringify(r.exceptionDetails), + ); + } + return r.result.value; + }; + + await cdp.send("Page.navigate", { url }, sessionId); + // Wait for the host app's own globals rather than a load event. + await waitFor( + () => evaluate("typeof window.rig === 'object' && !!window.supersetEmbeddedSdk"), + "the host app to load", + ); + return { evaluate, sessionId }; +} + +async function waitFor(predicate, what, timeoutMs = 15_000, intervalMs = 100) { + const deadline = Date.now() + timeoutMs; + let last; + for (;;) { + last = await predicate(); + if (last) return last; Review Comment: <!-- Bito Reply --> The reviewer is correct. In `openPage()`, the `waitFor` call is invoked directly without the `expect()` wrapper, meaning any rejection from the `evaluate()` predicate—such as a CDP error caused by a navigation destroying the execution context—will propagate and terminate the entire test run. Wrapping this call in a try-catch block or using a similar retry mechanism to the one implemented in `expect()` would allow the test to tolerate these transient failures until the timeout is reached. **superset-embedded-sdk/testrig/drive.mjs** ``` // Wait for the host app's own globals rather than a load event. await waitFor( () => evaluate("typeof window.rig === 'object' && !!window.supersetEmbeddedSdk"), "the host app to load", ); ``` ########## superset-embedded-sdk/testrig/host.html: ########## @@ -0,0 +1,208 @@ +<!doctype html> +<html lang="en"> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with this + work for additional information regarding copyright ownership. The ASF + licenses this file to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +--> +<head> + <meta charset="utf-8" /> + <title>embedded-sdk test rig — host app</title> + <style> + body { margin: 0; font: 14px/1.5 system-ui, sans-serif; display: grid; + grid-template-columns: 1fr 420px; height: 100vh; } + main { padding: 16px; overflow: auto; } + aside { border-left: 1px solid #ddd; padding: 16px; overflow: auto; background: #fafafa; } + h1 { font-size: 16px; margin: 0 0 12px; } + #mount { border: 1px solid #ccc; border-radius: 8px; height: 320px; } + #mount iframe { width: 100%; height: 100%; border: 0; border-radius: 8px; } + button { font: inherit; margin: 0 6px 6px 0; padding: 4px 10px; } + #log { font: 12px/1.45 ui-monospace, monospace; white-space: pre-wrap; } + .t { color: #888; } + .err { color: #b00; } + .ok { color: #070; } + </style> +</head> +<body> + <main> + <h1>Host app</h1> + <div> + <button id="embed">embed</button> + <button id="theme">setThemeMode(dark) + setThemeConfig</button> + <button id="tabs">getActiveTabs()</button> + <button id="unmount">unmount</button> + <button id="clear">clear log</button> + </div> + <div id="mount"></div> + <p class="t">Tokens minted by this host's endpoint: <b id="minted">0</b></p> + </main> + <aside><div id="log"></div></aside> + +<script src="/sdk.js"></script> +<script> + const SUPERSET_ORIGIN = "__SUPERSET_ORIGIN__"; + + // Everything the driver reads. + const rig = (window.rig = { + events: [], // what the embedded page reported over its side channel + log: [], // what the host app saw + errors: [], // rejections the SDK handed back + tokenFetches: 0, // calls the SDK made to fetchGuestToken + dashboard: null, + embedState: "idle", // pending | resolved | rejected + embedError: null, + heldFirstFetch: null, + }); + + function log(kind, ...parts) { + const line = `${new Date().toISOString().slice(11, 23)} ${kind} ${parts + .map((p) => (typeof p === "string" ? p : JSON.stringify(p))) + .join(" ")}`; + rig.log.push(line); + const el = document.createElement("div"); + el.className = kind === "error" ? "err" : kind === "ok" ? "ok" : ""; + el.textContent = line; + document.getElementById("log").prepend(el); + } + + // The embedded page's side channel (not the SDK's MessageChannel). + window.addEventListener("message", (event) => { + if (!event.data?.__rig) return; + rig.events.push({ ...event.data, at: Date.now() }); + log("guest", `page ${event.data.page}`, event.data.event, event.data.detail ?? ""); + }); + + async function refreshStats() { + const stats = await fetch("/stats").then((r) => r.json()); + document.getElementById("minted").textContent = stats.tokensMinted; + rig.tokensMinted = stats.tokensMinted; + return stats; + } + window.rigRefreshStats = refreshStats; + + // TTL is a query param so a run can exercise the refresh timer in seconds + // rather than minutes. + const search = new URLSearchParams(location.search); + const ttl = search.get("ttl") || "300"; + // `?slowfirst=1` holds the very first fetchGuestToken() until the driver + // settles it by hand, which is the only way to have a navigation happen + // while the initial token is still in flight. `?failnth=N` makes call N + // reject, standing in for a host endpoint that is down. + const slowFirst = search.get("slowfirst"); + const failNth = Number(search.get("failnth") || 0); + + async function mintToken() { + const { token } = await fetch(`/guest-token?ttl=${ttl}`).then((r) => r.json()); + refreshStats(); + return token; + } + + async function fetchGuestToken() { + rig.tokenFetches += 1; + const call = rig.tokenFetches; + log("host", `fetchGuestToken() #${call}`); + if (call === failNth) { + log("error", `fetchGuestToken() #${call} rejects (rig)`); + throw new Error(`rig: host token endpoint is down (call #${call})`); + } + if (slowFirst && call === 1) { + log("host", "fetchGuestToken() #1 held by the rig"); + return new Promise((resolve, reject) => { + rig.heldFirstFetch = { resolve, reject }; + }); + } + return mintToken(); + } + + // Settle the held first fetch, long after the SDK asked for it. + window.rigReleaseFirstToken = async () => { + const token = await mintToken(); + rig.heldFirstFetch.resolve(token); + log("host", "released the held fetchGuestToken() #1"); + }; + window.rigFailFirstToken = () => { + rig.heldFirstFetch.reject(new Error("rig: host token endpoint is down")); + log("error", "failed the held fetchGuestToken() #1"); + }; Review Comment: <!-- Bito Reply --> The suggestion is correct and should be applied. The current implementation of `rigReleaseFirstToken` and `rigFailFirstToken` lacks checks for `rig.heldFirstFetch`, which causes a TypeError if called when no fetch is held. Additionally, the proposed fix ensures that if `mintToken()` fails, the promise is properly handled, preventing the SDK's `fetchGuestToken` from hanging indefinitely. **superset-embedded-sdk/testrig/host.html** ``` window.rigReleaseFirstToken = async () => { if (!rig.heldFirstFetch) return; const token = await mintToken(); rig.heldFirstFetch.resolve(token); log("host", "released the held fetchGuestToken() #1"); }; window.rigFailFirstToken = () => { if (!rig.heldFirstFetch) return; rig.heldFirstFetch.reject(new Error("rig: host token endpoint is down")); log("error", "failed the held fetchGuestToken() #1"); }; ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
