Last active
December 10, 2023 22:41
-
-
Save akovalev/ccba919e362e75cb893bd5dbea39585f to your computer and use it in GitHub Desktop.
Happy Eyeballs implementation using Context
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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)); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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