Skip to content

Instantly share code, notes, and snippets.

@ericyd
Created November 3, 2023 15:07
Show Gist options
  • Save ericyd/666820a1ef73e1b0751f639ccefb2648 to your computer and use it in GitHub Desktop.
Save ericyd/666820a1ef73e1b0751f639ccefb2648 to your computer and use it in GitHub Desktop.
Async Validation with Generators
/**
* Running multiple validations asynchronously can present some interesting challenges,
* especially if the calling function should return a "result" type (or some other non-throwing value).
* There is even more complexity if you want to run the validations in sequence,
* and stop on the first failure.
*
* It is fairly easy to construct a meta function that executes the validators imperatively,
* but what if you want a more generic approach?
* This gist suggests a generic solution using generators, and slowly builds up the intuition
* until the final example shows a more realistic use case.
*/
/**
* [1] Sync generator, just for reference
*/
function *syncGen() {
for (let i = 0; i < 5; i++) {
yield i
}
}
function main1() {
for (const i of syncGen()) {
console.log(`sync n=${i}`)
}
}
// main1() // uncomment to see how it works
/** End [1] */
const sleep = async (n, name) => new Promise(resolve => setTimeout(() => {console.log(`${name} n=${n} (setTimeout)`); resolve(n)}, n * 30))
/**
* [2] simple async generator that waits for a simple timeout promise to resolve before yielding
* (don't mind the number in the name, it's just so numbers match between "main" and "asyncGen" functions)
*/
async function *asyncGen2(name) {
for (let i = 0; i < 5; i++) {
yield await sleep(i, name)
}
}
async function main2() {
for await (const i of asyncGen2('async2')) {
console.log(`async2 n=${i}`)
}
}
// main2() // uncomment to see how it works
/** End [2] */
/**
* [3] async generator demonstrating a sequence through a known list of promises
*/
async function *asyncGen3(name) {
const promises = Array(5).fill(0).map((_, i) => sleep(i, name))
// // this is the intuitive working solution
// for await (const i of promises) {
// yield i
// }
// this is kind of interesting that you don't actually need to explicitly await the promise
for (const i of promises) {
yield i
}
}
async function main3() {
for await (const i of asyncGen3('async3')) {
console.log(`async3 n=${i}`)
}
}
// main3() // uncomment to see how it works
/** end [3] */
/**
* [4] demonstrates that even if you interrupt the generator, if the promises are eagerly called
* in the generator, they will still resolve. This is not necessarily what we want.
* Key observation: "async4 n=2" is the highest number logged from this loop, but
* the setTimeout functions are logged all the way up to "async4 n=4 (setTimeout)"
*/
async function main4() {
for await (const i of asyncGen3('async4')) {
console.log(`async4 n=${i}`)
if (i === 2) break
}
}
// main4() // uncomment to see how it works
/** end [4] */
/**
* [5] is a way to structure the async generator so the promises are lazily called, and will not
* execute if the generator is interrupted.
* Key observation: logging ends at "async5 n=2"; the generator goes up to 4,
* but 3 and 4 are never called.
* This is a simple adjustment; you just need a list of functions that return the promise, instead
* of a list of actual promise.
*/
async function *asyncGen5(name) {
const promises = Array(5).fill(0).map((_, i) => () => sleep(i, name))
for (const fn of promises) {
yield fn()
}
}
async function main5() {
for await (const i of asyncGen5('async5')) {
console.log(`async5 n=${i}`)
if (i === 2) break
}
}
// main5() // uncomment to see how it works
/** end [5] */
/**
* [6] a more realistic example of processing multiple asynchronous validators
*/
/**
* @typedef {object} OkValidation
* @property {true} ok
*/
/**
* @typedef {object} ErrValidation
* @property {false} ok
* @property {string} error
*/
/**
* @callback Validator
* @param {any} input (arbitrary for demonstration)
* @returns {Promise<OkValidation | ErrValidation>}
*/
/**
* @type {Validator}
*/
async function validatorA() {
console.log(`executing validatorA`)
return { ok: true }
}
/**
* @type {Validator}
*/
async function validatorB(input) {
console.log(`executing validatorB`)
return { ok: false, error: `validatorB failed: ${input}` }
}
/**
* @type {Validator}
*/
async function validatorC(input) {
console.log(`executing validatorC: ${input}`)
return { ok: true }
}
async function *asyncValidation(input) {
const validators = [
validatorA,
() => validatorB(input),
() => validatorC(input),
]
for (const validator of validators) {
yield validator()
}
}
/**
* @typedef {object} Result
* @property {boolean} ok
* @property {string | null} error only present if ok is false
* @property {string} value only present if ok is true
*/
/**
* A demo function that returns.
* Key observation: `validatorC` will never be called, because `validatorB` fails.
* @returns {Promise<Result<string | Validation>>}
*/
async function main6() {
for await (const result of asyncValidation('async validation example')) {
if (!result.ok) {
return result
}
}
return { ok: true, value: 'completed all validations' }
}
// a little extra ceremony required to log the result without a top-level await
(async () => console.log('main6', await main6()))()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment