Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/__tests__/all-settled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,20 @@ describe("allSettled", () => {
expect(result.b).toEqual({ status: "fulfilled", value: 15 })
})

it("passes a non-aborted task signal when no external signal is configured", async () => {
let taskSignalAborted: boolean | undefined

const result = await try$.allSettled({
a() {
taskSignalAborted = this.$signal.aborted
return 1
},
})

expect(result.a).toEqual({ status: "fulfilled", value: 1 })
expect(taskSignalAborted).toBe(false)
})

it("rejects dependent task when referenced task fails", async () => {
const error = new Error("a failed")

Expand Down Expand Up @@ -180,6 +194,25 @@ describe("allSettled", () => {
expect(result.b).toEqual({ status: "fulfilled", value: "b done" })
})

it("keeps sibling task signals usable after another task fails", async () => {
const signalStates: boolean[] = []

const result = await try$.allSettled({
a() {
throw new Error("a failed")
},
async b() {
signalStates.push(this.$signal.aborted)
await sleep(10)
signalStates.push(this.$signal.aborted)
return "b done"
},
})

expect(signalStates).toEqual([false, false])
expect(result.b).toEqual({ status: "fulfilled", value: "b done" })
})

it("applies wrap middleware around allSettled execution", async () => {
let wrapCalls = 0

Expand All @@ -203,6 +236,33 @@ describe("allSettled", () => {
expect(wrapCalls).toBe(1)
})

it("runs wrap promise cleanup when allSettled() starts with an already-aborted signal", async () => {
const controller = new AbortController()
let cleaned = false

controller.abort(new Error("stop"))

try {
await try$
.wrap((_, next) =>
Promise.resolve(next()).finally(() => {
cleaned = true
})
)
.signal(controller.signal)
.allSettled({
a() {
return 1
},
})
expect.unreachable("should have thrown")
} catch (error) {
expect(error).toBeInstanceOf(CancellationError)
}

expect(cleaned).toBe(true)
})

it("honors cancellation signal from builder options", async () => {
const controller = new AbortController()

Expand Down Expand Up @@ -256,4 +316,29 @@ describe("allSettled", () => {

expect(cleaned).toBe(true)
})

it("runs disposer cleanup for both fulfilled and rejected tasks without external signals", async () => {
let cleanedA = false
let cleanedB = false

const result = await try$.allSettled({
a() {
this.$disposer.defer(() => {
cleanedA = true
})
return 1
},
b() {
this.$disposer.defer(() => {
cleanedB = true
})
throw new Error("boom")
},
})

expect(result.a).toEqual({ status: "fulfilled", value: 1 })
expect(result.b.status).toBe("rejected")
expect(cleanedA).toBe(true)
expect(cleanedB).toBe(true)
})
})
104 changes: 104 additions & 0 deletions src/__tests__/all.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ describe("all", () => {
expect(result).toEqual({ a: 10, b: 15 })
})

it("passes a non-aborted task signal when no external signal is configured", async () => {
let taskSignalAborted: boolean | undefined

const result = await try$.all({
a() {
taskSignalAborted = this.$signal.aborted
return 1
},
})

expect(result).toEqual({ a: 1 })
expect(taskSignalAborted).toBe(false)
})

it("rejects on first task failure", async () => {
try {
await try$.all({
Expand Down Expand Up @@ -119,6 +133,32 @@ describe("all", () => {
expect(signalAborted).toBe(true)
})

it("aborts dependency waiters when a sibling task fails", async () => {
let signalAbortedWhileWaiting = false

try {
await try$.all({
a() {
throw new Error("boom")
},
async b() {
try {
await this.$result.a
return 2
} catch (error) {
signalAbortedWhileWaiting = this.$signal.aborted
throw error
}
},
})
expect.unreachable("should have thrown")
} catch (error) {
expect((error as Error).message).toBe("boom")
}

expect(signalAbortedWhileWaiting).toBe(true)
})

it("rejects when a task accesses its own result", async () => {
try {
await try$.all({
Expand Down Expand Up @@ -249,6 +289,27 @@ describe("all", () => {
})
})

it("keeps catch-context signal usable after failure", async () => {
const result = await try$.all(
{
a() {
throw new Error("boom")
},
},
{
catch: (_error, ctx) => ({
aborted: ctx.signal.aborted,
hasSignal: ctx.signal instanceof AbortSignal,
}),
}
)

expect(result).toEqual({
aborted: true,
hasSignal: true,
})
})

it("throws Panic when all catch throws", async () => {
try {
await try$.all(
Expand Down Expand Up @@ -334,6 +395,33 @@ describe("all", () => {
expect(wrapCalls).toBe(1)
})

it("runs wrap promise cleanup when all() starts with an already-aborted signal", async () => {
const controller = new AbortController()
let cleaned = false

controller.abort(new Error("stop"))

try {
await try$
.wrap((_, next) =>
Promise.resolve(next()).finally(() => {
cleaned = true
})
)
.signal(controller.signal)
.all({
a() {
return 1
},
})
expect.unreachable("should have thrown")
} catch (error) {
expect(error).toBeInstanceOf(CancellationError)
}

expect(cleaned).toBe(true)
})

it("honors cancellation signal from builder options", async () => {
const controller = new AbortController()

Expand Down Expand Up @@ -380,6 +468,22 @@ describe("all", () => {
expect(cleaned).toBe(true)
})

it("allows disposer usage in a successful task without external cancellation", async () => {
let cleaned = false

const result = await try$.all({
a() {
this.$disposer.defer(() => {
cleaned = true
})
return 1
},
})

expect(result).toEqual({ a: 1 })
expect(cleaned).toBe(true)
})

it("runs disposer cleanup even on failure", async () => {
let cleaned = false

Expand Down
Loading