syncify-js
v0.0.4
Published
Provide a synchronous execution environment for asynchronous functions.
Maintainers
Readme
syncify-js
Highly recommended reading: Algebraic Effects for the Rest of Us
Install
npm install syncify-jsUsage
Simplest usage
import { withSync } from 'syncify-js'
const asyncFn1 = () => Promise.resolve(1)
const asyncFn2 = async () => 2
const main = () => {
return asyncFn1() + asyncFn2()
}
const data = await withSync(main, [asyncFn1, asyncFn2])
expect(data).toEqual([
3,
[
{ status: 'fulfilled', value: 1, name: 'asyncFn1' },
{ status: 'fulfilled', value: 2, name: 'asyncFn2' },
],
])onError
import { withSync } from 'syncify-js'
const asyncFn1 = async () => {
throw new Error()
}
const asyncFn2 = async () => {
throw new Error()
}
const main = () => {
return (asyncFn1() as unknown as number) + (asyncFn2() as unknown as number)
}
const data = await withSync(main, [
// if asyncFn1 throws an error, the result is 1
[asyncFn1, 1],
{ fn: asyncFn2, onError: () => 2 },
])
expect(data).toEqual([
3,
[
{ status: 'rejected', value: 1, name: 'asyncFn1' },
{ status: 'rejected', value: 2, name: 'asyncFn2' },
],
])Some error usages
You can examine the source code. I've constructed a new execution function using new Function and rebuilt the user-passed async function. However, some external variables are difficult to access through new Function, which has significant limitations. But in fact, if you understand the source code, you'll easily see that this is a compromise I made for user convenience. Any async function can overcome the limitations I mentioned by rewriting it according to the source code's approach.
- First Parameter Must Be a Function Reference (Not a Function Call)
withSync(() => main(), []) // ❌ withSync(main, []) // ✅ - Functions Cannot Access External Variables (Must Be Pure Functions)
// ❌ const a = 1 const main = () => { return a } withSync(main, []) // ✅ const main = () => { return 1 } withSync(main, [])
How it works
Let's use a simple example to illustrate. Suppose you want to implement a synchronously executing syncMain function:
const asyncFn = async () => 1
const main = () => {
const num = asyncFn()
console.log(num)
}
const syncMain = () => {
// TODO: implement
}The key to the implementation is: throw the Promise result of asyncFn as an exception, then execute the main function twice. The first execution throws an exception, and then we execute the main function again in the catch statement. Let's look at the code directly:
let asyncFn = async () => 1
let main = () => {
const num = asyncFn()
console.log(num)
}
const prevAsyncFn = asyncFn
const syncMain = () => {
const data = {
status: 'pending',
value: null,
}
// Modify the asyncFn function so that when executed, it throws the promise containing the final result
asyncFn = () => {
if (data.status === 'fulfilled') {
return data.value
}
if (data.status === 'rejected') {
throw data
}
// prevAsyncFn is an async function, so this is a promise
const promise = prevAsyncFn()
.then((res) => {
data.status = 'fulfilled'
data.value = res
})
.catch((err) => {
data.status = 'rejected'
data.value = err
})
// Throw this promise, which hasn't been resolved yet
throw promise
}
try {
// Since the asyncFn function will throw an exception, this will definitely enter the catch block
main()
} catch (err) {
if (err instanceof Promise) {
// This way, after .then, the result is resolved
err.then(main, main).finally(() => {
// Don't forget to restore asyncFn
asyncFn = prevAsyncFn
})
}
}
}
syncMain()