import { Task } from '@lit/task'; import { v4 as uuidv4 } from 'uuid'; import type { PlayerEmbed, PlayerEmbedProps } from '..'; import type { ReactiveControllerHost } from 'lit'; import type { Environment } from '~/utils/env'; import { getEnvUrls } from '~/utils/env'; type PlayerEmbedHost = ReactiveControllerHost & PlayerEmbed; type FetchTaskDeps = [ PlayerEmbedProps['env'], PlayerEmbedProps['apiKey'], PlayerEmbedProps['data'], PlayerEmbedProps['vouchId'], PlayerEmbedProps['templateId'] ]; type FilterTaskDeps = [PlayerEmbedProps['data'], PlayerEmbedProps['questions']]; class FetcherController { host: PlayerEmbedHost; private _fetching = false; private _vouch: PlayerEmbedProps['data']; set fetching(value) { if (this._fetching !== value) { this._fetching = value; this.host.requestUpdate(); } } get fetching() { return this._fetching; } private getVouch = async (env: Environment, apiKey: string, vouchId: string) => { const { embedApiUrl } = getEnvUrls(env); const cacheCheck = uuidv4(); const res = await fetch(`${embedApiUrl}/vouches/${vouchId}`, { method: 'GET', headers: [ ['X-Api-Key', apiKey], ['X-Cache-Check', cacheCheck] ] }); const vouch = await res.json(); this.host.dispatchEvent(new CustomEvent('vouch:loaded', { detail: vouch?.id })); // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic, // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another // API call with the `Cache-Control` header which will re-fill the cache const resCacheCheck = res?.headers?.get('X-Cache-Check'); if (resCacheCheck !== cacheCheck) { fetch(`${embedApiUrl}/vouches/${vouchId}`, { method: 'GET', headers: [ ['X-Api-Key', apiKey], ['Cache-Control', 'max-age=0'] ] }); } return vouch; }; private getTemplate = async (env: Environment, apiKey: string, templateId: string) => { const { embedApiUrl } = getEnvUrls(env); const cacheCheck = uuidv4(); const res = await fetch(`${embedApiUrl}/templates/${templateId}`, { method: 'GET', headers: [ ['X-Api-Key', apiKey], ['X-Cache-Check', cacheCheck] ] }); const template = await res.json(); // HACK: we're currently using API Gateway caching on the embed API without any invalidation logic, // so to ensure that the cache stays up to date, whenever we detect a cache hit we trigger another // API call with the `Cache-Control` header which will re-fill the cache const resCacheCheck = res?.headers?.get('X-Cache-Check'); if (resCacheCheck !== cacheCheck) { fetch(`${embedApiUrl}/templates/${templateId}`, { method: 'GET', headers: [ ['X-Api-Key', apiKey], ['Cache-Control', 'max-age=0'] ] }); } return template; }; constructor(host: PlayerEmbedHost) { this.host = host; new Task( this.host, async ([env, apiKey, data, vouchId, templateId]: FetchTaskDeps) => { try { host.vouch = undefined; host.template = undefined; if (data) { let template; if (templateId) { this.fetching = true; template = await this.getTemplate(env, apiKey, templateId); } this._vouch = data; host.template = template ?? data?.settings?.template?.instance; } else if (vouchId) { this.fetching = true; const [vouch, template] = await Promise.all([ this.getVouch(env, apiKey, vouchId), templateId ? this.getTemplate(env, apiKey, templateId) : null ]); this._vouch = vouch; host.template = template ?? vouch?.settings?.template?.instance; } } finally { this.fetching = false; } }, () => [host.env, host.apiKey, host.data, host.vouchId, host.templateId] ); // This second task is to be able to filter the vouch without fetching it again if only the questions changed new Task( this.host, ([vouch, questions]: FilterTaskDeps) => { host.vouch = vouch ? { ...vouch, questions: { items: vouch?.questions.items.filter((_, index) => !questions?.length || questions?.includes(index + 1)) } } : undefined; }, () => [this._vouch, host.questions] ); } } export { FetcherController };