1import { assert, describe, it, vi } from "@effect/vitest"
2import { Effect, Exit } from "effect"
3import * as Scheduler from "effect/Scheduler"
4
5describe("Scheduler", () => {
6 it("runSyncExit does not create a dispatcher for synchronous effects", () => {
7 const makeDispatcher = vi.spyOn(Scheduler.MixedScheduler.prototype, "makeDispatcher")
8 const exit = Effect.runSyncExit(Effect.sync(() => 1))
9 const calls = makeDispatcher.mock.calls.length
10 makeDispatcher.mockRestore()
11
12 assert.deepStrictEqual(exit, Exit.succeed(1))
13 assert.strictEqual(calls, 0)
14 })
15
16 it("runSyncExit flushes dispatcher work after yielding", () => {
17 const exit = Effect.runSyncExit(Effect.as(Effect.yieldNow, 1))
18
19 assert.deepStrictEqual(exit, Exit.succeed(1))
20 })
21
22 it("runSyncExit does not schedule timers after yielding", () => {
23 const setImmediate = vi.spyOn(globalThis, "setImmediate").mockImplementation(() => {
24 throw new Error("setImmediate is not supported")
25 })
26 const setTimeout = vi.spyOn(globalThis, "setTimeout").mockImplementation(() => {
27 throw new Error("setTimeout is not supported")
28 })
29
30 try {
31 const exit = Effect.runSyncExit(Effect.as(Effect.yieldNow, 1))
32
33 assert.deepStrictEqual(exit, Exit.succeed(1))
34 assert.strictEqual(setImmediate.mock.calls.length, 0)
35 assert.strictEqual(setTimeout.mock.calls.length, 0)
36 } finally {
37 setImmediate.mockRestore()
38 setTimeout.mockRestore()
39 }
40 })
41
42 it.effect("MixedScheduler orders by priority (sync)", () =>
43 Effect.sync(() => {
44 const scheduler = new Scheduler.MixedScheduler("sync").makeDispatcher()
45 const order: Array<string> = []
46
47 scheduler.scheduleTask(() => order.push("p0-1"), 0)
48 scheduler.scheduleTask(() => order.push("p10-1"), 10)
49 scheduler.scheduleTask(() => order.push("p-1-1"), -1)
50 scheduler.scheduleTask(() => order.push("p10-2"), 10)
51 scheduler.scheduleTask(() => order.push("p0-2"), 0)
52
53 assert.deepStrictEqual(order, [])
54
55 scheduler.flush()
56
57 assert.deepStrictEqual(order, [
58 "p-1-1",
59 "p0-1",
60 "p0-2",
61 "p10-1",
62 "p10-2"
63 ])
64 }))
65
66 it.effect("MixedScheduler is FIFO within a priority", () =>
67 Effect.sync(() => {
68 const scheduler = new Scheduler.MixedScheduler("sync").makeDispatcher()
69 const order: Array<number> = []
70
71 scheduler.scheduleTask(() => order.push(1), 5)
72 scheduler.scheduleTask(() => order.push(2), 5)
73 scheduler.scheduleTask(() => order.push(3), 5)
74
75 scheduler.flush()
76
77 assert.deepStrictEqual(order, [1, 2, 3])
78 }))
79
80 it.effect("PreventSchedulerYield disables shouldYield checks", () =>
81 Effect.gen(function*() {
82 let calls = 0
83 const scheduler: Scheduler.Scheduler = {
84 executionMode: "sync",
85 shouldYield: () => {
86 calls++
87 return false
88 },
89 makeDispatcher() {
90 return {} as any
91 }
92 }
93
94 yield* Effect.sync(() => undefined).pipe(
95 Effect.provideService(Scheduler.Scheduler, scheduler)
96 )
97 assert.strictEqual(calls > 0, true)
98
99 calls = 0
100 yield* Effect.sync(() => undefined).pipe(
101 Effect.provideService(Scheduler.Scheduler, scheduler),
102 Effect.provideService(Scheduler.PreventSchedulerYield, true)
103 )
104 assert.strictEqual(calls, 0)
105 }))
106
107 it.effect("inlined map steps count against MaxOpsBeforeYield", () =>
108 Effect.gen(function*() {
109 const order: Array<string> = []
110 const chain = (label: string) => {
111 let effect = Effect.succeed(0)
112 for (let i = 0; i < 100; i++) {
113 effect = Effect.map(effect, (n) => {
114 order.push(label)
115 return n + 1
116 })
117 }
118 return effect
119 }
120 yield* Effect.all([chain("a"), chain("b")], { concurrency: "unbounded" }).pipe(
121 Effect.provideService(Scheduler.MaxOpsBeforeYield, 16)
122 )
123 assert.notStrictEqual(order.join(""), "a".repeat(100) + "b".repeat(100))
124 }))
125
126 it("MixedScheduler falls back to a microtask when timers cannot be set", async () => {
127 // Cloudflare Workers throw for timers set in global scope
128 const setImmediate = vi.spyOn(globalThis, "setImmediate").mockImplementation(() => {
129 throw new Error("Disallowed operation called within global scope")
130 })
131 try {
132 const count = Scheduler.MaxOpsBeforeYield.defaultValue() * 3
133 const result = await Effect.runPromise(
134 Effect.forEach(Array.from({ length: count }, (_, i) => i), (i) => Effect.succeed(i))
135 )
136 assert.strictEqual(result.length, count)
137 assert.isAbove(setImmediate.mock.calls.length, 0)
138 } finally {
139 setImmediate.mockRestore()
140 }
141 })
142})