Skip to content

Commit 9a64651

Browse files
committed
feat: support resolve yield promise
1 parent 76b1b2e commit 9a64651

File tree

3 files changed

+112
-30
lines changed

3 files changed

+112
-30
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import { quansync } from 'quansync'
2828

2929
// Create an quansync function by providing `sync` and `async` implementations
3030
const readFile = quansync({
31-
sync: fs.readFileSync,
32-
async: fs.promises.readFile,
31+
sync: (path: string) => fs.readFileSync(path),
32+
async: (path: string) => fs.promises.readFile(path),
3333
})
3434

3535
// Create an quansync function by providing a generator function

src/index.ts

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
export interface QuansyncInputObject<Args extends any[], Return> {
1+
export interface QuansyncInputObject<Return, Args extends any[]> {
22
name?: string
33
sync: (...args: Args) => Return
44
async: (...args: Args) => Promise<Return>
55
}
6-
export type QuansyncInputGenerator<Args extends any[], Return>
6+
7+
export type QuansyncInputGenerator<Return, Args extends any[]>
78
= ((...args: Args) => QuansyncGenerator<Return>)
89

9-
export type QuansyncInput<Args extends any[], Return> =
10-
| QuansyncInputObject<Args, Return>
11-
| QuansyncInputGenerator<Args, Return>
10+
export type QuansyncInput<Return, Args extends any[]> =
11+
| QuansyncInputObject<Return, Args>
12+
| QuansyncInputGenerator<Return, Args>
1213

1314
export interface QuansyncYield<R> {
1415
name?: string
@@ -17,58 +18,75 @@ export interface QuansyncYield<R> {
1718
__isQuansync: true
1819
}
1920

20-
export type UnwrapQuansyncReturn<T> = T extends QuansyncYield<infer R> ? R : T
21+
export type UnwrapQuansyncReturn<T> = T extends QuansyncYield<infer R> ? Awaited<R> : Awaited<T>
2122

2223
export type QuansyncGenerator<Return = any, Yield = unknown> =
2324
Generator<Yield, Return, UnwrapQuansyncReturn<Yield>>
2425

2526
/**
2627
* "Superposition" function that can be consumed in both sync and async contexts.
2728
*/
28-
export type QuansyncFn<Args extends any[] = [], Return = any> =
29+
export type QuansyncFn<Return = any, Args extends any[] = []> =
2930
((...args: Args) => QuansyncGenerator<Return>)
3031
& {
3132
sync: (...args: Args) => Return
3233
async: (...args: Args) => Promise<Return>
3334
}
3435

36+
const ERROR_PROMISE_IN_SYNC = '[Quansync] Yielded an unexpected promise in sync context'
37+
38+
function isThenable<T>(value: any): value is Promise<T> {
39+
return value && typeof value === 'object' && typeof value.then === 'function'
40+
}
41+
3542
export function isQuansyncYield<T>(value: any | QuansyncYield<T>): value is QuansyncYield<T> {
3643
return typeof value === 'object' && value !== null && '__isQuansync' in value
3744
}
3845

39-
function fromObject<Args extends any[], Return>(
40-
options: QuansyncInputObject<Args, Return>,
41-
): QuansyncFn<Args, Return> {
46+
function fromObject<Return, Args extends any[]>(
47+
options: QuansyncInputObject<Return, Args >,
48+
): QuansyncFn<Return, Args > {
4249
const generator = function *(...args: Args): QuansyncGenerator<Return, any> {
4350
return yield {
4451
name: options.name,
45-
sync: () => options.sync(...args),
46-
async: () => options.async(...args),
52+
sync: (options.sync as any).bind(null, ...args),
53+
async: (options.async as any).bind(null, ...args),
4754
__isQuansync: true,
4855
}
49-
} as unknown as QuansyncFn<Args, Return>
56+
} as unknown as QuansyncFn<Return, Args>
5057

5158
generator.sync = options.sync
5259
generator.async = options.async
5360

5461
return generator
5562
}
5663

64+
function fromPromise<T>(promise: Promise<T> | T): QuansyncFn<T, []> {
65+
return fromObject({
66+
async: () => Promise.resolve(promise),
67+
sync: () => {
68+
if (isThenable(promise))
69+
throw new Error(ERROR_PROMISE_IN_SYNC)
70+
return promise
71+
},
72+
})
73+
}
74+
5775
function unwrapSync(value: any): any {
58-
return isQuansyncYield(value)
59-
? value.sync()
60-
: value
76+
if (isQuansyncYield(value))
77+
return value.sync()
78+
if (isThenable(value))
79+
throw new Error(ERROR_PROMISE_IN_SYNC)
80+
return value
6181
}
6282

6383
function unwrapAsync(value: any): Promise<any> {
64-
return isQuansyncYield(value)
65-
? value.async()
66-
: value
84+
return isQuansyncYield(value) ? value.async() : value
6785
}
6886

69-
function fromGenerator<Args extends any[], Return>(
70-
generator: QuansyncInputGenerator<Args, Return>,
71-
): QuansyncFn<Args, Return> {
87+
function fromGenerator<Return, Args extends any[]>(
88+
generator: QuansyncInputGenerator<Return, Args>,
89+
): QuansyncFn<Return, Args> {
7290
function sync(...args: Args): Return {
7391
const iterator = generator(...args)
7492
let current = iterator.next()
@@ -84,7 +102,7 @@ function fromGenerator<Args extends any[], Return>(
84102
while (!current.done) {
85103
current = iterator.next(await unwrapAsync(current.value))
86104
}
87-
return await unwrapAsync(current.value)
105+
return unwrapAsync(current.value)
88106
}
89107

90108
return fromObject({
@@ -97,11 +115,20 @@ function fromGenerator<Args extends any[], Return>(
97115
/**
98116
* Creates a new Quansync function, a "superposition" between async and sync.
99117
*/
100-
export function quansync<Args extends any[], Return>(
101-
options: QuansyncInput<Args, Return>,
102-
): QuansyncFn<Args, Return> {
118+
export function quansync<Return, Args extends any[] = []>(
119+
options: QuansyncInput<Return, Args> | Promise<Return>,
120+
): QuansyncFn<Return, Args> {
121+
if (isThenable(options))
122+
return fromPromise<Return>(options)
103123
if (typeof options === 'function')
104124
return fromGenerator(options)
105125
else
106126
return fromObject(options)
107127
}
128+
129+
/**
130+
* Converts a promise to a Quansync generator.
131+
*/
132+
export function promiseToGenerator<T>(promise: Promise<T> | T): QuansyncGenerator<T> {
133+
return fromPromise(promise as Promise<T>)()
134+
}

test/index.test.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { expect, it } from 'vitest'
2-
import { isQuansyncYield, quansync } from '../src'
2+
import { isQuansyncYield, promiseToGenerator, quansync } from '../src'
33

44
it('basic', async () => {
55
const add = quansync({
@@ -49,9 +49,64 @@ it('generator', async () => {
4949
const value = yield * add(sum, a)
5050
sum = value
5151
}
52-
return yield * toString(sum)
52+
const foo = yield * toString(sum)
53+
return foo
5354
})
5455

5556
expect(multiply.sync(2, 3)).toBe('6')
5657
expect(await multiply.async(4, 5)).toBe('20')
5758
})
59+
60+
it('yield optional promise', async () => {
61+
interface Transformer {
62+
transform: (code: string) => string | Promise<string>
63+
}
64+
65+
const transform = quansync(function *(transformers: Transformer[], code: string) {
66+
let current = code
67+
for (const transformer of transformers) {
68+
current = yield * promiseToGenerator(transformer.transform(current))
69+
// ...
70+
}
71+
return current
72+
})
73+
74+
expect(transform.sync([], '')).toBe('')
75+
await expect(transform.async([], '')).resolves.toBe('')
76+
77+
expect(
78+
transform.sync([
79+
{
80+
transform: (code: string) => `${code}1`,
81+
},
82+
], 'foo'),
83+
).toBe('foo1')
84+
expect(() =>
85+
transform.sync([
86+
{
87+
transform: async (code: string) => `${code}1`,
88+
},
89+
], 'foo'),
90+
).toThrowErrorMatchingInlineSnapshot(`[Error: [Quansync] Yielded an unexpected promise in sync context]`)
91+
92+
await expect(
93+
transform.async([
94+
{
95+
transform: async (code: string) => `${code}1`,
96+
},
97+
], 'foo'),
98+
).resolves.toBe('foo1')
99+
})
100+
101+
it('yield promise', async () => {
102+
const run = quansync(function *(code: string) {
103+
const result = yield new Promise<string>((resolve) => {
104+
setTimeout(() => resolve(code), 10)
105+
})
106+
return result
107+
})
108+
109+
expect(() => run.sync('foo'))
110+
.toThrowErrorMatchingInlineSnapshot(`[Error: [Quansync] Yielded an unexpected promise in sync context]`)
111+
await expect(run.async('foo')).resolves.toBe('foo')
112+
})

0 commit comments

Comments
 (0)