How We Handle Data Requests and Cancellations in Wope

kadiryazici
wopehq
Published in
3 min readApr 4, 2024

In Wope, we ditch the overhead of complex query libraries and directly send requests when components mount. While data is being retrieved, a loading screen keeps users informed. However, to prevent unnecessary requests and maintain efficiency, we’ve implemented a simple solution called Request Pool.

Request Pool

Whenever we send a GET request within a component or scope, we simply wrap it with the collect method. This method works behind the scenes by creating a special signal and attaching it to the dispatched requests. If a request ever needs to be cancelled (because plans change!), this signal swoops in and gracefully stops it, preventing unnecessary data from being fetched for users.

A Practical Example: Using Request Pool for Get Request

Here’s a concrete example demonstrating how to leverage Request Pool in your Vue components:

import { ref } from "vue";
import axios from "axios";

import { useRequestPool } from "./requestPool";

const requestPool = useRequestPool();
const loading = ref(false);
const ourData = ref([]);

async function sendRequest() {
try {
const { data } = await requestPool.collect("key", () => {
loading.value = true;
ourData.value = [];

return axios.get("https://example.com");
});

ourData.value = data;
} catch (error) {
if (axios.isCancel(error)) {
console.log("Request is canceled by request pool");
}
} finally {
loading.value = false;
}
}

You might be wondering why we set the loading value to true within the collect callback instead of before using the method. There’s a reason for this! Request Pool can’t predict cancellations before you call collect. To keep things running synchronously, we run our side effects within the callback.

Request Pool also cancels all requests when the component is unmounted. This ensures that no data is fetched when it’s no longer needed.

Sending multiple requests with Request Pool

Because the collect method is synchronous, sending multiple requests at once isn’t possible. To achieve this, we use the Promise.all method:

const [first, second] = await requestPool.collect("key", () => {
loading.value = true;

return Promise.all([
axios.get("https://example.com/1"),
axios.get("https://example.com/2"),
]);
});

Under the Hood: How Request Pool Works

When you utilize the collect method, Request Pool modifies a global variable named currentSignal. Additionally, a custom Axios interceptor is in place to retrieve and attach this signal to the actual request before it’s sent out.

A Peek at the Custom Axios Interceptor:

import axios from "axios";
import { getCurrentSignal } from "./requestPool";

axios.interceptors.request.use((config) => {
const signal = getCurrentSignal();

if (signal) {
config.signal = signal;
}

return config;
});

Request Pool Implementation:

import { onScopeDispose, getCurrentScope } from "vue";

let currentSignal: AbortSignal | null = null;

export function getCurrentSignal() {
return currentSignal;
}

export function useRequestPool() {
const pool = new Map<string, AbortController>();

function collect<T extends Promise<any>>(key: string, callback: () => T): T {
pool.get(key)?.abort();
const controller = new AbortController();
currentSignal = controller.signal;
const returnValue = callback();
currentSignal = null;
pool.set(key, controller);

return returnValue.finally(() => {
pool.delete(key);
});
}

function cancelAll() {
for (const controller of pool.values()) {
controller.abort();
}

pool.clear();
}

function cancel(key: string) {
const controller = pool.get(key);

if (controller) {
controller.abort();
pool.delete(key);
}
}

if (getCurrentScope()) onScopeDispose(cancelAll);

return {
collect,
cancel,
cancelAll,
};
}

We hope this explanation provides a clear understanding of how Request Pool simplifies data request management and races in Wope!

--

--