|
| 1 | +import { SurfaceEmbed } from "./embed/embed"; |
| 2 | +import { setSurfaceConsent, type SurfaceConsent } from "./consent/consent"; |
| 3 | +import type { EmbedTypeInput, SurfaceEmbedOptions } from "./types"; |
| 4 | + |
| 5 | +/** |
| 6 | + * Replays `SurfaceEmbed` / `SurfaceSetConsent` calls a page made before this |
| 7 | + * script finished loading. Lets the tag be embedded with `async` (it no longer |
| 8 | + * has to block the HTML parser just so `window.SurfaceEmbed` exists for later |
| 9 | + * inline scripts). |
| 10 | + * |
| 11 | + * The counterpart is a small inline stub the install snippet places BEFORE the |
| 12 | + * async script tag (source of truth for the generated snippet: |
| 13 | + * `surface_forms` `lib/shared/surfaceTagSnippet.ts` — keep the entry shape in |
| 14 | + * sync with `StubQueueEntry`): |
| 15 | + * |
| 16 | + * ```html |
| 17 | + * <script> |
| 18 | + * window.SurfaceTagQueue = window.SurfaceTagQueue || []; |
| 19 | + * window.SurfaceEmbed = window.SurfaceEmbed || function () { |
| 20 | + * var entry = { type: "embed", args: arguments, calls: [], stub: this, |
| 21 | + * questionId: document.currentScript && document.currentScript.getAttribute("data-question-id") }; |
| 22 | + * var stub = this; |
| 23 | + * ["showSurfaceForm", "hideSurfaceForm", "initializeEmbed"].forEach(function (method) { |
| 24 | + * stub[method] = function () { entry.calls.push(method); }; |
| 25 | + * }); |
| 26 | + * window.SurfaceTagQueue.push(entry); |
| 27 | + * }; |
| 28 | + * window.SurfaceSetConsent = window.SurfaceSetConsent || function (consent) { |
| 29 | + * window.SurfaceTagQueue.push({ type: "consent", args: [consent] }); |
| 30 | + * }; |
| 31 | + * </script> |
| 32 | + * ``` |
| 33 | + * |
| 34 | + * The stub captures `data-question-id` from the constructing script tag at |
| 35 | + * queue time because `document.currentScript` is null by the time the drain |
| 36 | + * runs (input-trigger embeds route on it). |
| 37 | + */ |
| 38 | + |
| 39 | +const RELAYED_METHODS = [ |
| 40 | + "showSurfaceForm", |
| 41 | + "hideSurfaceForm", |
| 42 | + "initializeEmbed", |
| 43 | +] as const; |
| 44 | + |
| 45 | +type RelayedMethod = (typeof RELAYED_METHODS)[number]; |
| 46 | + |
| 47 | +interface StubQueueEntry { |
| 48 | + type: "embed" | "consent"; |
| 49 | + args: ArrayLike<unknown>; |
| 50 | + questionId?: string | null; |
| 51 | + calls?: RelayedMethod[]; |
| 52 | + stub?: Record<string, unknown>; |
| 53 | +} |
| 54 | + |
| 55 | +export function drainStubQueue(): void { |
| 56 | + const w = window as unknown as { SurfaceTagQueue?: unknown }; |
| 57 | + if (!Array.isArray(w.SurfaceTagQueue)) return; |
| 58 | + |
| 59 | + const entries = w.SurfaceTagQueue as StubQueueEntry[]; |
| 60 | + // Swap the array out first: code that captured the stub function before the |
| 61 | + // tag loaded still pushes here, and those pushes must process immediately |
| 62 | + // instead of landing in a queue nobody reads again. A non-array queue also |
| 63 | + // makes a double-loaded tag skip draining twice. |
| 64 | + w.SurfaceTagQueue = { push: processEntry }; |
| 65 | + entries.forEach(processEntry); |
| 66 | +} |
| 67 | + |
| 68 | +function processEntry(entry: StubQueueEntry): void { |
| 69 | + try { |
| 70 | + if (entry?.type === "consent") { |
| 71 | + setSurfaceConsent((entry.args?.[0] ?? {}) as Partial<SurfaceConsent>); |
| 72 | + return; |
| 73 | + } |
| 74 | + if (entry?.type !== "embed") return; |
| 75 | + |
| 76 | + const [src, embedType, targetClass, options] = Array.from(entry.args ?? []); |
| 77 | + const embed = new SurfaceEmbed( |
| 78 | + src as string, |
| 79 | + embedType as EmbedTypeInput, |
| 80 | + targetClass as string, |
| 81 | + { |
| 82 | + ...((options as SurfaceEmbedOptions) ?? {}), |
| 83 | + ...(entry.questionId ? { questionId: entry.questionId } : {}), |
| 84 | + } |
| 85 | + ); |
| 86 | + |
| 87 | + for (const method of entry.calls ?? []) { |
| 88 | + if (RELAYED_METHODS.includes(method)) embed[method](); |
| 89 | + } |
| 90 | + |
| 91 | + // The page may be holding the stub instance (`const c = new SurfaceEmbed(...)`) |
| 92 | + // — re-point its methods at the real embed so later calls work. |
| 93 | + if (entry.stub && typeof entry.stub === "object") { |
| 94 | + for (const method of RELAYED_METHODS) { |
| 95 | + entry.stub[method] = () => embed[method](); |
| 96 | + } |
| 97 | + } |
| 98 | + } catch (error) { |
| 99 | + // A malformed entry must never break the host page or later entries. |
| 100 | + console.error("Surface Tag: failed to replay queued call", error); |
| 101 | + } |
| 102 | +} |
0 commit comments