Skip to content

Commit

Permalink
chore: use @lit/task library to manage async tasks for fetching the b…
Browse files Browse the repository at this point in the history
…anner from the auction (#42)
  • Loading branch information
PabloReszczynski authored and anonvt committed Jul 1, 2024
1 parent 92609a1 commit 4543c2c
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 158 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"vite-plugin-dts": "^3.8.3"
},
"dependencies": {
"@lit/task": "^1.0.1",
"lit": "^3.1.3",
"skeleton-webcomponent-loader": "^2.1.4"
}
Expand Down
10 changes: 10 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

254 changes: 96 additions & 158 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Task } from "@lit/task";
import { LitElement, type TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { customElement, property } from "lit/decorators.js";
import { TopsortConfigurationError, TopsortRequestError } from "./errors";
import type { Auction, Banner, BannerState } from "./types";

/* Set up global environment for TS_BANNERS */

Expand All @@ -9,7 +11,7 @@ declare global {
TS_BANNERS: {
getLink(banner: Banner): string;
getLoadingElement(): HTMLElement;
getErrorElement(error: Error): HTMLElement;
getErrorElement(error: unknown): HTMLElement;
getNoWinnersElement(): HTMLElement;
getBannerElement(banner: Banner): HTMLElement;
};
Expand Down Expand Up @@ -44,50 +46,6 @@ const getDeviceType = (): "mobile" | "desktop" => {
return "desktop";
};

interface Loading {
status: "loading";
}

interface Errored {
status: "errored";
error: Error;
}

interface NoWinners {
status: "nowinners";
}

interface Ready {
status: "ready";
banner: Banner;
}

interface Auction {
type: "banners";
slots: 1;
device: "mobile" | "desktop";
slotId: string;
category?: {
id?: string;
ids?: string[];
disjunctions?: string[][];
};
geoTargeting?: {
location: string;
};
searchQuery?: string;
}

/** The banner object returned from the auction request */
export interface Banner {
type: "product" | "vendor" | "brand" | "url";
id: string;
resolvedBidId: string;
asset: [{ url: string }];
}

type BannerState = Loading | Errored | NoWinners | Ready;

/**
* A banner web component that runs an auction and renders the winning banner.
*/
Expand Down Expand Up @@ -117,10 +75,10 @@ export class TopsortBanner extends LitElement {
@property({ attribute: "location", type: String })
readonly location?: string;

@state()
private state: BannerState = {
status: "loading",
};
private task = new Task(this, {
task: this.runAuction,
args: () => [this.buildAuction()],
});

private getLink(banner: Banner): string {
if (window.TS_BANNERS.getLink) {
Expand All @@ -137,25 +95,26 @@ export class TopsortBanner extends LitElement {
const element = window.TS_BANNERS.getLoadingElement();
return html`${element}`;
}
return html`<div class="ts-banner-${this.state.status}">Loading</div>`;
// By default, hide the component while loading
return html``;
}

private getErrorElement(error: Error): TemplateResult {
private getErrorElement(error: unknown): TemplateResult {
if (window.TS_BANNERS.getErrorElement) {
const element = window.TS_BANNERS.getErrorElement(error);
return html`${element}`;
}
return html`<div class="ts-banner-${this.state.status}>
<pre>${error.message}</pre>
</div>`;
// By default, hide the component if there is an error
return html``;
}

private getNoWinnersElement(): TemplateResult {
if (window.TS_BANNERS.getNoWinnersElement) {
const element = window.TS_BANNERS.getNoWinnersElement();
return html`${element}`;
}
return html`<div class="ts-banner-${this.state.status}"></div>`;
// By default, hide the component if there are no winners
return html``;
}

private getBannerElement(banner: Banner): TemplateResult {
Expand All @@ -175,133 +134,112 @@ export class TopsortBanner extends LitElement {
<div style="${style}"
data-ts-clickable
data-ts-resolved-bid=${banner.resolvedBidId}
class="ts-banner-${this.state.status}">
class="ts-banner">
<a href="${href}">
<img src="${src}" alt="Topsort banner"></img>
</a>
</div>
`;
}

private setState(state: BannerState) {
this.state = state;
private emitEvent(status: string) {
const event = new CustomEvent("statechange", {
detail: { state, slotId: this.slotId },
detail: { slotId: this.slotId, status },
bubbles: true,
composed: true,
});
this.dispatchEvent(event);
}

private async runAuction() {
private buildAuction(): Auction {
const device = getDeviceType();
try {
const auction: Auction = {
type: "banners",
slots: 1,
device,
slotId: this.slotId,
const auction: Auction = {
type: "banners",
slots: 1,
device,
slotId: this.slotId,
};
if (this.categoryId) {
auction.category = {
id: this.categoryId,
};
if (this.categoryId) {
auction.category = {
id: this.categoryId,
};
} else if (this.categoryIds) {
auction.category = {
ids: this.categoryIds.split(",").map((item) => item.trim()),
};
} else if (this.categoryDisjunctions) {
auction.category = {
disjunctions: [this.categoryDisjunctions.split(",").map((item) => item.trim())],
};
} else if (this.searchQuery) {
auction.searchQuery = this.searchQuery;
}
if (this.location) {
auction.geoTargeting = {
location: this.location,
};
}
const token = window.TS.token;
const url = window.TS.url || "https://api.topsort.com";
const res = await fetch(new URL(`${url}/v2/auctions`), {
method: "POST",
mode: "cors",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"X-UA": `topsort/banners-${import.meta.env.PACKAGE_VERSION} (${device}})`,
},
body: JSON.stringify({
auctions: [auction],
}),
});
if (res.ok) {
const data = await res.json();
const result = data.results[0];
if (result) {
if (result.error) {
logError(result.error);
this.setState({
status: "errored",
error: Error("Unknown Error"),
});
} else if (result.winners[0]) {
const winner = result.winners[0];
this.setState({
status: "ready",
banner: winner,
});
} else {
this.setState({
status: "nowinners",
});
}
}
} else {
const error = await res.json();
logError(error);
this.setState({
status: "errored",
error: new TopsortRequestError(error.message, res.status),
});
}
} catch (err) {
logError(err);
if (err instanceof Error) {
this.setState({
status: "errored",
error: err,
});
} else {
this.setState({
status: "errored",
error: Error("Unknown Error"),
});
}
} else if (this.categoryIds) {
auction.category = {
ids: this.categoryIds.split(",").map((item) => item.trim()),
};
} else if (this.categoryDisjunctions) {
auction.category = {
disjunctions: [this.categoryDisjunctions.split(",").map((item) => item.trim())],
};
} else if (this.searchQuery) {
auction.searchQuery = this.searchQuery;
}
if (this.location) {
auction.geoTargeting = {
location: this.location,
};
}
return auction;
}

// Runs when DOM is loaded. Much like React's `getInitialProps`
connectedCallback() {
super.connectedCallback();
this.runAuction();
private async runAuction(
[auction]: Auction[],
{ signal }: { signal: AbortSignal },
): Promise<BannerState> {
const device = getDeviceType();
const token = window.TS.token;
const url = window.TS.url || "https://api.topsort.com";
const res = await fetch(new URL(`${url}/v2/auctions`), {
method: "POST",
mode: "cors",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"X-UA": `topsort/banners-${import.meta.env.PACKAGE_VERSION} (${device}})`,
},
body: JSON.stringify({
auctions: [auction],
}),
signal,
});
if (!res.ok) {
const error = await res.json();
logError(error);
throw new Error(error.message);
}
const data = await res.json();
const result = data.results[0];
if (!result) throw new TopsortRequestError("No auction results", res.status);
if (result.error) {
logError(result.error);
throw new Error(result.error);
}
if (result.winners.length) {
return {
status: "ready",
banners: result.winners,
};
}
return {
status: "nowinners",
};
}

protected render() {
if (!window.TS.token || !this.slotId) {
return this.getErrorElement(new TopsortConfigurationError(window.TS.token, this.slotId));
}
switch (this.state.status) {
case "ready":
return this.getBannerElement(this.state.banner);
case "nowinners":
return this.getNoWinnersElement();
case "loading":
return this.getLoadingElement();
case "errored":
return this.getErrorElement(this.state.error);
}
return this.task.render({
pending: () => this.getLoadingElement(),
complete: (value) => {
this.emitEvent(value.status);
if (value.status === "nowinners") {
return this.getNoWinnersElement();
}
return this.getBannerElement(value.banners[0]);
},
error: (error) => this.getErrorElement(error),
});
}

// avoid shadow dom since we cannot attach to events via analytics.js
Expand Down
Loading

0 comments on commit 4543c2c

Please sign in to comment.