We want to create a realistic illustration of how async generators can deal with timing issues in ad rendering.
Here we're considering the example of multiple ad slots for the same provider in a scrolling feed. In our example, a Repository can only initiate an ad fetch to populate the AdSlot component when the AdContainer component comes into view. What we want is an observer to emit a visible
event from the AdContainer that signals the async iterator to emit the next ad index, which we need because the ad fetches are paginated. On the other end of this chain, when the AdSlot is rendered (on the visible AdContainer), we want to emit a viewed
event with the correct ad slot data.
This pattern effectively decouples the UI interaction (scrolling) from the data logic (fetching), creating a clean "pull-based" system from "push-based" UI events.
Here is a complete, runnable Node.js example that simulates this ad-loading flow.
We'll simulate the components and events using classes and a central EventEmitter
.
adEvents
(EventEmitter): A single, shared event bus for all components.AdContainer
: Simulates a container that emits a'containerVisible'
event when it scrolls into view.AdSlot
: Simulates the component that renders the ad and emits a'adViewed'
event.AdRepository
: Simulates fetching ad data from a server.adIndexProvider
(Async Generator): The core of our solution. It listens for'containerVisible'
events and yields the next ad index.main
(Orchestrator): The main loop that consumes indices from the generator, fetches data, and renders the ads.
// This example requires Node.js to run.
import { EventEmitter } from 'node:events';
import { setTimeout } from 'node:timers/promises';
// A central event bus for our application
const adEvents = new EventEmitter();
// --- 1. The Simulated UI Components and Data Layer ---
class AdContainer {
constructor(id) {
this.id = id;
this.adSlot = new AdSlot(this.id);
}
// Simulate this container scrolling into the viewport
simulateBecameVisible() {
console.log(`\nποΈ Container #${this.id} is now VISIBLE.`);
// Push an event to signal we need an ad
adEvents.emit('containerVisible', { containerId: this.id });
}
}
class AdSlot {
constructor(id) {
this.id = id;
}
// Render the ad data and fire a tracking event
render(adData) {
console.log(` β‘οΈ AdSlot #${this.id} is RENDERING ad: "${adData.creative}"`);
// Push a tracking event
adEvents.emit('adViewed', { slotId: this.id, adData: adData });
}
}
class AdRepository {
// Simulate a paginated API call to fetch ad data
async fetchAdByIndex(index) {
console.log(` β³ Fetching ad for index ${index}...`);
await setTimeout(250); // Simulate network latency
const adData = {
id: `AD-${index}-XYZ`,
creative: `Dynamic ad #${index + 1}: Buy Now!`,
cpm: Math.random().toFixed(2),
};
console.log(` β
Fetched ad for index ${index}.`);
return adData;
}
}
// --- 2. The Core Async Generator ---
/**
* An async generator that listens for 'containerVisible' events
* and yields the next sequential ad index. This is the bridge
* between the UI event and the data-fetching logic.
*/
async function* adIndexProvider(emitter) {
let currentIndex = 0;
let resolveNext = null;
const listener = () => {
// When a 'visible' event occurs, open the promise "gate"
if (resolveNext) {
resolveNext();
resolveNext = null;
}
};
emitter.on('containerVisible', listener);
try {
while (true) {
// Pause here and wait for the listener to call resolveNext()
await new Promise((resolve) => {
resolveNext = resolve;
});
// Once un-paused, yield the current index and increment
yield currentIndex++;
}
} finally {
// Important: Clean up the listener to prevent memory leaks
emitter.off('containerVisible', listener);
console.log('[Generator] Cleaned up and shut down.');
}
}
// --- 3. The Orchestration Logic ---
async function main() {
console.log('--- Ad Loading System Initialized ---');
// Setup our application
const repository = new AdRepository();
const adContainers = [new AdContainer(1), new AdContainer(2), new AdContainer(3)];
// Setup a listener for our final tracking event
adEvents.on('adViewed', ({ slotId, adData }) => {
console.log(` π TRACKING: Ad view for Slot #${slotId}, Creative ID: ${adData.id}`);
});
// Get the async iterator from our generator
const adIndexIterator = adIndexProvider(adEvents);
// This loop is the consumer. It will pause indefinitely at each
// iteration until a 'containerVisible' event is emitted.
for await (const adIndex of adIndexIterator) {
console.log(`π Main loop received ad index: ${adIndex}`);
// We know an ad is needed, but which container needs it?
// In a real app, the event payload would tell us. Here we'll just assume they appear in order.
const targetContainer = adContainers[adIndex];
if (!targetContainer) {
console.log('--- All ad slots filled. Shutting down. ---');
break; // Exit the loop if we run out of containers
}
try {
const adData = await repository.fetchAdByIndex(adIndex);
targetContainer.adSlot.render(adData);
} catch (e) {
console.error(`Failed to load ad for index ${adIndex}:`, e);
}
}
}
// --- 4. Simulate User Scrolling ---
async function simulateUserScrolling() {
const containers = [new AdContainer(1), new AdContainer(2), new AdContainer(3)];
// We need to pass the *same* containers to the main logic
// so let's adjust the main function slightly to accept them.
// This is a more realistic dependency injection pattern.
// Start the main ad-loading logic in the background
const adLoadingProcess = orchestrateAdLoading(containers);
// Now, simulate the user scrolling down the page
for (const container of containers) {
await setTimeout(1500); // User scrolls...
container.simulateBecameVisible();
}
// Wait for the main loop to finish (or it will run forever)
// To stop it, we can call iterator.return()
await setTimeout(500);
adIndexIterator.return(); // This triggers the 'finally' block in the generator
await adLoadingProcess;
}
// Modified main function to be more testable
async function orchestrateAdLoading(adContainers) {
const repository = new AdRepository();
// ... (setup tracking listener)
adIndexIterator = adIndexProvider(adEvents); // a global iterator for simplicity here
// ... (the for await loop)
// ...
// All the logic from the original `main` function would go here.
// To avoid code duplication, I'll just call the original main and
// start the simulation part that triggers it.
}
let adIndexIterator; // Let's make this accessible to stop it
const repository = new AdRepository();
const adContainers = [new AdContainer(1), new AdContainer(2), new AdContainer(3)];
adEvents.on('adViewed', ({ slotId, adData }) => {
console.log(` π TRACKING: Ad view for Slot #${slotId}, Creative ID: ${adData.id}`);
});
adIndexIterator = adIndexProvider(adEvents);
const adLoadingProcess = (async () => {
console.log('--- Ad Loading System Initialized ---');
for await (const adIndex of adIndexIterator) {
console.log(`π Main loop received ad index: ${adIndex}`);
const targetContainer = adContainers[adIndex];
if (!targetContainer) {
console.log('--- All ad slots filled. ---');
break;
}
const adData = await repository.fetchAdByIndex(adIndex);
targetContainer.adSlot.render(adData);
}
})();
// This part simulates the user's actions, which drive the whole process.
(async () => {
await setTimeout(1000);
adContainers[0].simulateBecameVisible();
await setTimeout(2000);
adContainers[1].simulateBecameVisible();
await setTimeout(1500);
adContainers[2].simulateBecameVisible();
await setTimeout(500);
adIndexIterator.return(); // Gracefully stop the generator
await adLoadingProcess;
console.log('--- Simulation Complete ---');
})();
- Initialization: The
orchestrateAdLoading
function starts. It creates theadIndexIterator
and immediately enters thefor await...of
loop. - The Pause: The loop pauses on the first iteration, waiting for the iterator to yield a value. Inside the generator, this corresponds to
await new Promise(...)
. The entire ad-loading system is now idle, waiting for a signal. - The "Push" Event: The simulation waits for 1.5 seconds, then calls
adContainers[0].simulateBecameVisible()
. This fires a'containerVisible'
event. - The Bridge: The listener inside
adIndexProvider
hears this event and callsresolveNext()
. This resolves the promise that the generator was awaiting. - The "Pull" Action: The generator wakes up,
yield
s the current index (0
), and increments it. Thefor await
loop receives the value0
and its body begins to execute. - Data Flow: The main loop calls the repository to fetch the ad, and upon success, tells the
AdSlot
to render it. TheAdSlot
then fires the final'adViewed'
tracking event. - Repeat: The
for await
loop finishes its body and goes back to the top, again pausing and waiting for the next value from the iterator. The whole system goes idle again until the user scrolls and the nextsimulateBecameVisible()
is called. - Termination: Calling
adIndexIterator.return()
forces the generator to enter itsfinally
block, which cleans up the event listener and allows the program to exit gracefully.
This architecture creates a beautiful separation of concerns. The view components (AdContainer
) know nothing about the data layer, and the data layer knows nothing about the view. They are orchestrated by a central loop that pulls work to be done in response to pushed events from the UI.
Here's a similar example but for the browser.