feat(codemode): support Promise.any and new Promise construction (#36339)
This commit is contained in:
parent
59202356fe
commit
fe09a2e9b7
10 changed files with 741 additions and 47 deletions
|
|
@ -1062,15 +1062,179 @@ describe("unsupported promise surface", () => {
|
|||
})
|
||||
|
||||
test("unknown Promise statics list what is available", async () => {
|
||||
const diagnostic = await error(`return await Promise.any([tools.host.sleepy({ id: 1 })])`)
|
||||
expect(diagnostic.message).toContain("Promise.any is not available")
|
||||
expect(diagnostic.message).toContain("Promise.allSettled")
|
||||
})
|
||||
|
||||
test("new Promise(...) points at tool calls instead", async () => {
|
||||
const diagnostic = await error(`return new Promise((resolve) => resolve(1))`)
|
||||
expect(diagnostic.kind).toBe("UnsupportedSyntax")
|
||||
expect(diagnostic.message).toContain("new Promise(...) is not supported")
|
||||
expect(diagnostic.message).toContain("already return promises")
|
||||
const diagnostic = await error(`return await Promise.withResolvers()`)
|
||||
expect(diagnostic.message).toContain("Promise.withResolvers is not available")
|
||||
expect(diagnostic.message).toContain("Promise.any")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Promise.any", () => {
|
||||
test("first tool success wins; failing and losing calls are handled silently", async () => {
|
||||
const trace = makeTrace()
|
||||
const result = await run(
|
||||
`
|
||||
const winner = await Promise.any([
|
||||
tools.host.fail({}),
|
||||
tools.host.sleepy({ id: 1, ms: 5 }),
|
||||
tools.host.sleepy({ id: 2, ms: 60000 }),
|
||||
])
|
||||
return winner
|
||||
`,
|
||||
{ trace },
|
||||
)
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
expect(result.value).toBe(1)
|
||||
// The slow loser stays execution-owned and is interrupted at completion; the tool
|
||||
// failure was observed by the aggregate, so no rejection warning survives.
|
||||
expect(result.warnings).toBeUndefined()
|
||||
expect(trace.interrupted).toBe(1)
|
||||
})
|
||||
|
||||
test("all members failing rejects with catch-normalized reasons in input order", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
try {
|
||||
await Promise.any([tools.host.fail({}), Promise.reject("plain")])
|
||||
return "fulfilled"
|
||||
} catch (error) {
|
||||
return [error.name, error.errors.map((reason) => reason.message ?? reason)]
|
||||
}
|
||||
`),
|
||||
).toEqual(["AggregateError", ["Lookup refused", "plain"]])
|
||||
})
|
||||
|
||||
test("settles one reaction turn after its deciding member, as in V8", async () => {
|
||||
expect(await value(`return await Promise.race([Promise.any([Promise.resolve(1)]), Promise.resolve(2)])`)).toBe(2)
|
||||
})
|
||||
|
||||
test("a tie is decided by settlement order, not input order", async () => {
|
||||
// Handlers run in attach order, so `first` settles before `second` and wins
|
||||
// despite its later input position - as in real JS.
|
||||
expect(
|
||||
await value(`
|
||||
const first = Promise.resolve().then(() => "one")
|
||||
const second = Promise.resolve().then(() => "two")
|
||||
return await Promise.any([second, first])
|
||||
`),
|
||||
).toBe("one")
|
||||
})
|
||||
|
||||
test("an abandoned rejecting aggregate is interrupted silently at the return", async () => {
|
||||
const result = await run(`
|
||||
Promise.any([Promise.reject(new Error("boom"))])
|
||||
return "done"
|
||||
`)
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
expect(result.value).toBe("done")
|
||||
expect(result.warnings).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("promise construction", () => {
|
||||
test("a deferred gate coordinates tool results across async functions", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let openGate
|
||||
const gate = new Promise((resolve) => { openGate = resolve })
|
||||
const worker = (async () => {
|
||||
const id = await gate
|
||||
return id * 2
|
||||
})()
|
||||
openGate(await tools.host.sleepy({ id: 21, ms: 5 }))
|
||||
return await worker
|
||||
`),
|
||||
).toBe(42)
|
||||
})
|
||||
|
||||
test("the .then(resolve) bridge settles a constructed promise", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const bridged = new Promise((resolve, reject) => {
|
||||
tools.host.sleepy({ id: 7, ms: 5 }).then(resolve, reject)
|
||||
})
|
||||
return await bridged
|
||||
`),
|
||||
).toBe(7)
|
||||
})
|
||||
|
||||
test("constructed promises participate in combinators", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let settle
|
||||
const manual = new Promise((resolve) => { settle = resolve })
|
||||
const race = Promise.race([manual, tools.host.sleepy({ id: 3, ms: 60000 })])
|
||||
const all = Promise.all([manual, "plain"])
|
||||
const any = Promise.any([manual, new Promise(() => {})])
|
||||
settle("manual")
|
||||
return [await race, await all, await any]
|
||||
`),
|
||||
).toEqual(["manual", ["manual", "plain"], "manual"])
|
||||
})
|
||||
|
||||
test("resolving with a pending promise adopts its later settlement", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
let innerResolve, innerReject
|
||||
const adopted = new Promise((resolve) => resolve(new Promise((resolve) => { innerResolve = resolve })))
|
||||
const adoptedRejection = new Promise((resolve) => resolve(new Promise((_, reject) => { innerReject = reject })))
|
||||
innerResolve("later")
|
||||
innerReject("bad")
|
||||
try {
|
||||
return [await adopted, await adoptedRejection]
|
||||
} catch (reason) {
|
||||
return [await adopted, reason]
|
||||
}
|
||||
`),
|
||||
).toEqual(["later", "bad"])
|
||||
})
|
||||
|
||||
test("an async executor's post-await resolve settles the promise", async () => {
|
||||
expect(
|
||||
await value(`
|
||||
const result = new Promise(async (resolve) => {
|
||||
const id = await tools.host.sleepy({ id: 5, ms: 5 })
|
||||
resolve(id * 2)
|
||||
})
|
||||
return await result
|
||||
`),
|
||||
).toBe(10)
|
||||
})
|
||||
|
||||
test("a never-settled promise is abandoned silently at the return", async () => {
|
||||
const result = await run(`
|
||||
const forever = new Promise(() => {})
|
||||
forever.then(() => {})
|
||||
return "done"
|
||||
`)
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
expect(result.value).toBe("done")
|
||||
expect(result.warnings).toBeUndefined()
|
||||
})
|
||||
|
||||
test("an un-awaited constructed rejection is reported like any unhandled rejection", async () => {
|
||||
const result = await run(`
|
||||
new Promise((_, reject) => reject(new Error("dropped")))
|
||||
await Promise.resolve()
|
||||
await Promise.resolve()
|
||||
return "done"
|
||||
`)
|
||||
expect(result.ok).toBe(true)
|
||||
if (!result.ok) return
|
||||
expect(result.value).toBe("done")
|
||||
expect(result.warnings).toHaveLength(1)
|
||||
expect(result.warnings?.[0].message).toContain("Unhandled rejection")
|
||||
expect(result.warnings?.[0].message).toContain("dropped")
|
||||
})
|
||||
|
||||
test("resolver functions cannot cross the data boundary", async () => {
|
||||
const diagnostic = await error(`
|
||||
let escaped
|
||||
new Promise((resolve) => { escaped = resolve })
|
||||
return { escaped }
|
||||
`)
|
||||
expect(diagnostic.kind).toBe("InvalidDataValue")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue