Skip to content

Instantly share code, notes, and snippets.

@akovalev
Last active December 10, 2023 22:41
Show Gist options
  • Save akovalev/ccba919e362e75cb893bd5dbea39585f to your computer and use it in GitHub Desktop.
Save akovalev/ccba919e362e75cb893bd5dbea39585f to your computer and use it in GitHub Desktop.
Happy Eyeballs implementation using Context
// inspired by https://golang.org/pkg/context/
function noop() {
}
function makeBarrier() {
let open;
const whenOpened = new Promise((resolve, reject) => {
open = resolve;
});
return {open, whenOpened};
}
function makeContext(whenCancelled) {
const children = new Set();
whenCancelled.then((e) => children.forEach(cancel => cancel(e)));
return {
whenCancelled,
children
};
}
function whenCancelled(ctx, fn) {
return ctx.whenCancelled;
}
function withCancel(parent) {
const {open: cancel, whenOpened: whenCancelled} = makeBarrier();
const ctx = makeContext(whenCancelled);
parent.children.add(cancel);
whenCancelled.then(() => parent.children.delete(cancel), noop);
return [ctx, cancel];
}
function withTimeout(parent, ms) {
const [ctx, cancel] = withCancel(parent);
delay(parent, ms).then(
() => cancel(new Error("context expired")),
noop
);
return ctx;
}
function delay(ctx, ms) {
return new Promise((resolve, reject) => {
const timerId = setTimeout(resolve, ms);
whenCancelled(ctx).then((e) => {
clearTimeout(timerId);
reject(e);
});
});
}
const background = makeContext(new Promise(noop));
const imageUrls = [
'https://idagio-images.global.ssl.fastly.net/albums/636943613627/main.jpg',
'https://idagio-images.global.ssl.fastly.net/albums/636943613627/error.jpg',
// 'https://idagio-images.global.ssl.fastly.net/albums/636943613627/error.jpg',
'https://idagio-images.global.ssl.fastly.net/albums/636943613627/main.jpg',
'https://idagio-images.global.ssl.fastly.net/albums/636943613627/main.jpg',
'https://idagio-images.global.ssl.fastly.net/albums/636943613627/main.jpg',
// 'https://idagio-images.global.ssl.fastly.net/albums/636943613627/error.jpg',
// 'https://idagio-images.global.ssl.fastly.net/albums/636943613627/error.jpg',
'https://idagio-images.global.ssl.fastly.net/albums/636943613627/error.jpg',
// 'https://idagio-images.global.ssl.fastly.net/albums/636943613627/main.jpg',
// 'https://idagio-images.global.ssl.fastly.net/albums/636943613627/main.jpg',
];
happyEyeballs(
imageUrls.map((url) => (ctx) => fetchBufferWithContext(ctx, url)),
{
timeout: 7000,
attemptDelay: 5000
}
)
// happyEyeballs({urls: imageUrls, timeout: 20000, attemptDelay: 5000})
.then(result => console.log(">>> result", result))
.catch(e => console.log(">>> error", e));
///////////////////////////////////////////////////
function fetchBufferWithContext(ctx, url) {
const controller = new AbortController();
whenCancelled(ctx).then(() => controller.abort());
return fetch(url, {signal: controller.signal})
.then((resp) => {
if (resp.ok) {
return resp.arrayBuffer();
} else {
throw `Fetch error: ${resp.status} - ${resp.statusText}`
}
});
}
// Context-based implementation of Happy Eyeballs algorithm
// See: https://tools.ietf.org/html/rfc6555#section-6
function happyEyeballs(tasks, {timeout, attemptDelay}) {
let failures = 0;
const [scope, cancelScope] = withCancel(background);
function attempt(idx, resolve, reject) {
const [taskCtx, cancelTask] = withCancel(scope);
tasks[idx](taskCtx)
.then(resolve)
.catch(() => {
cancelTask();
if (++failures === tasks.length) reject();
});
// schedule next attempt
if (idx < (tasks.length - 1)) {
// wait for the current attempt to fail or timeout to expire
Promise.race([whenCancelled(taskCtx), delay(scope, attemptDelay)])
.then(() => attempt(idx + 1, resolve, reject))
.catch(() => console.log(`attempt #${idx} failed or canceled`))
}
}
setTimeout(cancelScope, timeout);
return new Promise((resolve, reject) => attempt(0, resolve, reject))
// and cancel root context and all its children (other attempts)
// as soon as one of attempts succeeds or all attempts fail
.finally(cancelScope);
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Context</title>
</head>
<body>
<script src="context.js"></script>
<script src="example.js"></script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment