const { content } = compile(`<script setup>${code}</script>`, {
refSugar: true
})
+ if (shouldAsync) {
+ expect(content).toMatch(`let __temp, __restore`)
+ }
expect(content).toMatch(`${shouldAsync ? `async ` : ``}setup(`)
if (typeof expected === 'string') {
expect(content).toMatch(expected)
}
test('expression statement', () => {
- assertAwaitDetection(`await foo`, `await _withAsyncContext(foo)`)
+ assertAwaitDetection(
+ `await foo`,
+ `;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())`
+ )
})
test('variable', () => {
assertAwaitDetection(
`const a = 1 + (await foo)`,
- `1 + (await _withAsyncContext(foo))`
+ `1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))`
)
})
test('ref', () => {
assertAwaitDetection(
`ref: a = 1 + (await foo)`,
- `1 + (await _withAsyncContext(foo))`
+ `1 + ((([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore(),__temp))`
)
})
test('nested statements', () => {
assertAwaitDetection(`if (ok) { await foo } else { await bar }`, code => {
return (
- code.includes(`await _withAsyncContext(foo)`) &&
- code.includes(`await _withAsyncContext(bar)`)
+ code.includes(
+ `;(([__temp,__restore]=_withAsyncContext(()=>(foo))),__temp=await __temp,__restore())`
+ ) &&
+ code.includes(
+ `;(([__temp,__restore]=_withAsyncContext(()=>(bar))),__temp=await __temp,__restore())`
+ )
)
})
})
LabeledStatement,
CallExpression,
RestElement,
- TSInterfaceBody
+ TSInterfaceBody,
+ AwaitExpression
} from '@babel/types'
import { walk } from 'estree-walker'
import { RawSourceMap } from 'source-map'
})
}
+ /**
+ * await foo()
+ * -->
+ * (([__temp, __restore] = withAsyncContext(() => foo())),__temp=await __temp,__restore(),__temp)
+ */
+ function processAwait(node: AwaitExpression, isStatement: boolean) {
+ s.overwrite(
+ node.start! + startOffset,
+ node.argument.start! + startOffset,
+ `${isStatement ? `;` : ``}(([__temp,__restore]=${helper(
+ `withAsyncContext`
+ )}(()=>(`
+ )
+ s.appendLeft(
+ node.end! + startOffset,
+ `))),__temp=await __temp,__restore()${isStatement ? `` : `,__temp`})`
+ )
+ }
+
function processRefExpression(exp: Expression, statement: LabeledStatement) {
if (exp.type === 'AssignmentExpression') {
const { left, right } = exp
node.type.endsWith('Statement')
) {
;(walk as any)(node, {
- enter(node: Node) {
- if (isFunction(node)) {
+ enter(child: Node, parent: Node) {
+ if (isFunction(child)) {
this.skip()
}
- if (node.type === 'AwaitExpression') {
+ if (child.type === 'AwaitExpression') {
hasAwait = true
- s.prependRight(
- node.argument.start! + startOffset,
- helper(`withAsyncContext`) + `(`
- )
- s.appendLeft(node.argument.end! + startOffset, `)`)
+ processAwait(child, parent.type === 'ExpressionStatement')
}
}
})
if (propsIdentifier) {
s.prependRight(startOffset, `\nconst ${propsIdentifier} = __props`)
}
+ // inject temp variables for async context preservation
+ if (hasAwait) {
+ const any = isTS ? `:any` : ``
+ s.prependRight(startOffset, `\nlet __temp${any}, __restore${any}\n`)
+ }
const destructureElements =
hasDefineExposeCall || !options.inlineTemplate ? [`expose`] : []
const Comp = defineComponent({
async setup() {
+ let __temp: any, __restore: any
+
beforeInstance = getCurrentInstance()
- const msg = await withAsyncContext(
- new Promise(r => {
- resolve = r
- })
- )
+
+ const msg = (([__temp, __restore] = withAsyncContext(
+ () =>
+ new Promise(r => {
+ resolve = r
+ })
+ )),
+ (__temp = await __temp),
+ __restore(),
+ __temp)
+
// register the lifecycle after an await statement
onMounted(spy)
afterInstance = getCurrentInstance()
const Comp = defineComponent({
async setup() {
+ let __temp: any, __restore: any
+
beforeInstance = getCurrentInstance()
try {
- await withAsyncContext(
- new Promise((r, rj) => {
- reject = rj
- })
+ ;[__temp, __restore] = withAsyncContext(
+ () =>
+ new Promise((_, rj) => {
+ reject = rj
+ })
)
+ __temp = await __temp
+ __restore()
} catch (e) {
// ignore
}
const Comp = defineComponent({
async setup() {
+ let __temp: any, __restore: any
+
beforeInstance = getCurrentInstance()
+
// first await
- await withAsyncContext(Promise.resolve())
+ ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
+ __temp = await __temp
+ __restore()
+
// setup exit, instance set to null, then resumed
- await withAsyncContext(doAsyncWork())
+ ;[__temp, __restore] = withAsyncContext(() => doAsyncWork())
+ __temp = await __temp
+ __restore()
+
afterInstance = getCurrentInstance()
return () => {
resolve()
const Comp = defineComponent({
async setup() {
- await withAsyncContext(Promise.resolve())
- await withAsyncContext(Promise.reject())
+ let __temp: any, __restore: any
+ ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
+ __temp = await __temp
+ __restore()
+ ;[__temp, __restore] = withAsyncContext(() => Promise.reject())
+ __temp = await __temp
+ __restore()
},
render() {}
})
expect(getCurrentInstance()).toBeNull()
})
+ // #4050
+ test('race conditions', async () => {
+ const uids = {
+ one: { before: NaN, after: NaN },
+ two: { before: NaN, after: NaN }
+ }
+
+ const Comp = defineComponent({
+ props: ['name'],
+ async setup(props: { name: 'one' | 'two' }) {
+ let __temp: any, __restore: any
+
+ uids[props.name].before = getCurrentInstance()!.uid
+ ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
+ __temp = await __temp
+ __restore()
+
+ uids[props.name].after = getCurrentInstance()!.uid
+ return () => ''
+ }
+ })
+
+ const app = createApp(() =>
+ h(Suspense, () =>
+ h('div', [h(Comp, { name: 'one' }), h(Comp, { name: 'two' })])
+ )
+ )
+ const root = nodeOps.createElement('div')
+ app.mount(root)
+
+ await new Promise(r => setTimeout(r))
+ expect(uids.one.before).not.toBe(uids.two.before)
+ expect(uids.one.before).toBe(uids.one.after)
+ expect(uids.two.before).toBe(uids.two.after)
+ })
+
test('should teardown in-scope effects', async () => {
let resolve: (val?: any) => void
const ready = new Promise(r => {
const Comp = defineComponent({
async setup() {
- await withAsyncContext(Promise.resolve())
+ let __temp: any, __restore: any
+ ;[__temp, __restore] = withAsyncContext(() => Promise.resolve())
+ __temp = await __temp
+ __restore()
c = computed(() => {})
// register the lifecycle after an await statement
import { isPromise } from '../../shared/src'
import {
getCurrentInstance,
+ setCurrentInstance,
SetupContext,
- createSetupContext,
- setCurrentInstance
+ createSetupContext
} from './component'
import { EmitFn, EmitsOptions } from './componentEmits'
import {
}
/**
- * Runtime helper for storing and resuming current instance context in
- * async setup().
+ * `<script setup>` helper for persisting the current instance context over
+ * async/await flows.
+ *
+ * `@vue/compiler-sfc` converts the following:
+ *
+ * ```ts
+ * const x = await foo()
+ * ```
+ *
+ * into:
+ *
+ * ```ts
+ * let __temp, __restore
+ * const x = (([__temp, __restore] = withAsyncContext(() => foo())),__temp=await __temp,__restore(),__temp)
+ * ```
+ * @internal
*/
-export function withAsyncContext<T>(awaitable: T | Promise<T>): Promise<T> {
+export function withAsyncContext(getAwaitable: () => any) {
const ctx = getCurrentInstance()
- setCurrentInstance(null) // unset after storing instance
- if (__DEV__ && !ctx) {
- warn(`withAsyncContext() called when there is no active context instance.`)
+ let awaitable = getAwaitable()
+ setCurrentInstance(null)
+ if (isPromise(awaitable)) {
+ awaitable = awaitable.catch(e => {
+ setCurrentInstance(ctx)
+ throw e
+ })
}
- return isPromise<T>(awaitable)
- ? awaitable.then(
- res => {
- setCurrentInstance(ctx)
- return res
- },
- err => {
- setCurrentInstance(ctx)
- throw err
- }
- )
- : (awaitable as any)
+ return [awaitable, () => setCurrentInstance(ctx)]
}