diff --git a/lib/future.ts b/lib/future.ts index b61517651..6c922a899 100644 --- a/lib/future.ts +++ b/lib/future.ts @@ -1,9 +1,10 @@ import { lazyPromiseWithResolvers } from "./lazy-promise.ts"; -import type { Future } from "./types.ts"; +import type { Future, Operation } from "./types.ts"; import { withResolvers } from "./with-resolvers.ts"; export interface FutureWithResolvers { future: Future; + operation: Operation; resolve(value: T): void; reject(error: Error): void; } @@ -24,6 +25,7 @@ export function createFuture(): FutureWithResolvers { let future = Object.defineProperties(promise.promise, { [Symbol.iterator]: { enumerable: false, + configurable: true, value: operation.operation[Symbol.iterator], }, [Symbol.toStringTag]: { @@ -33,5 +35,5 @@ export function createFuture(): FutureWithResolvers { }, }) as Future; - return { future, resolve, reject }; + return { future, operation: operation.operation, resolve, reject }; } diff --git a/lib/task.ts b/lib/task.ts index c25ce5b0e..ad323686e 100644 --- a/lib/task.ts +++ b/lib/task.ts @@ -33,7 +33,10 @@ export function createTask(options: TaskOptions): NewTask { return Object.defineProperties(Object.create(Promise.prototype), { [Symbol.iterator]: { enumerable: false, - value: destroy, + *value() { + yield* assertNonCircular(scope); + yield* destroy(); + }, }, then: { enumerable: false, @@ -58,7 +61,10 @@ export function createTask(options: TaskOptions): NewTask { }, [Symbol.iterator]: { enumerable: false, - value: future.future[Symbol.iterator], + *value() { + yield* assertNonCircular(scope); + return yield* future.operation; + }, }, [Symbol.toStringTag]: { enumerable: false, @@ -147,3 +153,31 @@ export function* trap(operation: () => Operation): Operation { }) as T; } } + +function* assertNonCircular(scope: Scope) { + let caller = (yield* useScope()) as ScopeInternal; + + if (isSelfOrAncestor(scope as ScopeInternal, caller)) { + throw new CircularTaskError(); + } +} + +function isSelfOrAncestor(scope: ScopeInternal, child: ScopeInternal) { + for ( + let contexts = child.contexts; + contexts; + contexts = Object.getPrototypeOf(contexts) + ) { + if (scope.contexts === contexts) { + return true; + } + } + return false; +} + +class CircularTaskError extends Error { + constructor() { + super("a task may not depend on itself"); + this.name = "CircularTaskError"; + } +} diff --git a/test/run.test.ts b/test/run.test.ts index 376bc6fc8..468700241 100644 --- a/test/run.test.ts +++ b/test/run.test.ts @@ -202,29 +202,6 @@ describe("run()", () => { await expect(task).rejects.toMatchObject({ message: "boom" }); }); - it("can halt itself", async () => { - let task: Task = run(function* () { - yield* sleep(0); - yield* task.halt(); - }); - - await expect(task).rejects.toMatchObject({ message: "halted" }); - }); - - it("can halt itself between yield points", async () => { - let task: Task = run(function* root() { - yield* sleep(0); - - yield* spawn(function* child() { - yield* task.halt(); - }); - - yield* suspend(); - }); - - await expect(task).rejects.toMatchObject({ message: "halted" }); - }); - it("can delay halt if child fails", async () => { let didRun = false; let task = run(function* Main() { @@ -386,4 +363,144 @@ describe("run()", () => { expect(error?.message).toEqual("halted"); expect(halted).toEqual(true); }); + + describe("task dependencies", () => { + it("throws an error if a task depends on itself", async () => { + await expect(run(function* () { + let task: Task = yield* spawn(function* () { + yield* sleep(1); + yield* task; + return "hello world"; + }); + return yield* task; + })).rejects.toMatchObject({ name: "CircularTaskError" }); + }); + + it("throws an error if a task depends on its own halt()", async () => { + await expect(run(function* () { + let task: Task = yield* spawn(function* () { + yield* sleep(1); + yield* task.halt(); + return "hello world"; + }); + return yield* task; + })).rejects.toMatchObject({ name: "CircularTaskError" }); + }); + + it("throws an error if a task depends on its parent's halt()", async () => { + let parent: Task; + parent = run(function* () { + yield* spawn(function* () { + yield* sleep(1); + yield* parent.halt(); + }); + yield* suspend(); + }); + await expect(parent).rejects.toMatchObject({ name: "CircularTaskError" }); + }); + + it("throws an error if a task depends on its parent", async () => { + let parent: Task; + parent = run(function* () { + yield* spawn(function* () { + yield* sleep(1); + yield* parent; + }); + yield* suspend(); + }); + await expect(parent).rejects.toMatchObject({ name: "CircularTaskError" }); + }); + + it("throws an error if a task depends on an ancestor's halt()", async () => { + let root: Task; + root = run(function* () { + yield* spawn(function* () { + yield* spawn(function* () { + yield* sleep(1); + yield* root.halt(); + }); + yield* suspend(); + }); + yield* suspend(); + }); + await expect(root).rejects.toMatchObject({ name: "CircularTaskError" }); + }); + + it("throws an error if a task depends on an ancestor", async () => { + let root: Task; + root = run(function* () { + yield* spawn(function* () { + yield* spawn(function* () { + yield* sleep(1); + yield* root; + }); + yield* suspend(); + }); + yield* suspend(); + }); + await expect(root).rejects.toMatchObject({ name: "CircularTaskError" }); + }); + + it("allows a task to depend on a sibling", async () => { + let observed = await run(function* () { + let scope = yield* useScope(); + let producer = scope.run(function* () { + yield* sleep(1); + return "produced"; + }); + let consumer = scope.run(function* () { + return yield* producer; + }); + return yield* consumer; + }); + expect(observed).toEqual("produced"); + }); + + it("allows a task to depend on a sibling's halt()", async () => { + let halted = false; + await run(function* () { + let scope = yield* useScope(); + let target = scope.run(function* () { + try { + yield* suspend(); + } finally { + halted = true; + } + }); + yield* sleep(1); + let halter = scope.run(function* () { + yield* target.halt(); + }); + yield* halter; + }); + expect(halted).toEqual(true); + }); + + it("allows a task to depend on its child", async () => { + let observed = await run(function* () { + let child = yield* spawn(function* () { + yield* sleep(1); + return "child-result"; + }); + return yield* child; + }); + expect(observed).toEqual("child-result"); + }); + + it("allows a task to depend on its child's halt()", async () => { + let halted = false; + await run(function* () { + let child = yield* spawn(function* () { + try { + yield* suspend(); + } finally { + halted = true; + } + }); + yield* sleep(1); + yield* child.halt(); + }); + expect(halted).toEqual(true); + }); + }); });