Skip to content

Push and pull

Data sources are often modelled as either a push or pull.

For example, in Javascript DOM events, if you want the position of the cursor, it needs to be pushed to you via an event handler. You can’t pull the values at any time.

  • Push is characterised as being event-driven, where the source seems more in control. You’ll see this in DOM events, but is also akin to physical metaphors like parcel delivery. It tends to be that values gets pushed when they change.

  • Pull is more receiver-oriented, requesting values when it wants. For example, read the value of an analog pin on a microcontroller or checking your mailbox to see if there’s mail.

Sources might be inherently - or logically - push or pull, and it’s worth being aware when you’re fighting against its logic. And if so, what challenges this poses.

Polling

It’s common to poll pull sources - continually read the value and take action based on the current value or when it changes.

A challenge when polling is choosing how often to do so. This will usually be a trade-off between latency and resource usage.

A high polling rate (ie. reading very fast) will lower latency, giving values which more closely match the source. However, the device will end up spending a lot time reading data, leaving less resources for processing the data or using it constructively.

In cases where data sources don’t change often, but could in principle change at any time (eg pressing a mouse button), it becomes very difficult to imagine a perfect polling strategy. We’d be checking all the time if a button is pressed, but really we only want to know when it is pressed.

It’s a bit like going to a parcel pickup location every day in case a parcel is available for you (polling) versus waiting for a notification that a parcel is ready for pickup (push).

Adapting

Sometimes we have a push source we want to use as a pull, or vice versa.

From push to pull

Let’s convert a push event into something that can be pulled:

let lastCoord = { x:0,y:0 }
window.addEventListener(`pointermove`, event => {
lastCoord = { x: event.x, y: event.y };
})
const getLastCoord = () => lastCoord;

Now we have a pull function to call, getLastCoord where we can read the last pointer coordinate (or of course we could access the variable lastCoord directly).

This can be implemented efficiently, with little trade-offs in terms of performance or code.

From pull to push

Converting from a pull to push is where things get more challenging.

Based on polling, we can run a callback when the value changes:

function monitorSensor(callback) {
let lastValue = -1;
setInterval(() => {
const read = readSensor();
if (read !== lastValue) {
callback(read);
lastValue = read;
}
}, 5); // read every 5ms
}

Now we have push-style access:

monitorSensor(value => {
console.log(`New value is: ${value}`);
});

We could also push values based a different logic - for example sending a value when a threshold is reached, or only when it changes by a certain amount.

As described earlier under Polling, a challenge is in setting the right polling rate. In the above example, we poll at a constant 5ms rate. But this is regardless of whether the monitorSensor callback cares about values at that time.

The ixfx Reactive pattern allows for push (and sometimes pull) semantics. Generators are interesting in allowing a hybrid of push and pull.