| 1 | import { assert, describe, it, vi } from "@effect/vitest" |
| 2 | import { Effect, Exit } from "effect" |
| 3 | import * as Scheduler from "effect/Scheduler" |
| 4 | |
| 5 | describe("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 | }) |