Skip to content
Open
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
6 changes: 4 additions & 2 deletions lib/future.ts
Original file line number Diff line number Diff line change
@@ -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<T> {
future: Future<T>;
operation: Operation<T>;
resolve(value: T): void;
reject(error: Error): void;
}
Expand All @@ -24,6 +25,7 @@ export function createFuture<T>(): FutureWithResolvers<T> {
let future = Object.defineProperties(promise.promise, {
[Symbol.iterator]: {
enumerable: false,
configurable: true,
value: operation.operation[Symbol.iterator],
},
[Symbol.toStringTag]: {
Expand All @@ -33,5 +35,5 @@ export function createFuture<T>(): FutureWithResolvers<T> {
},
}) as Future<T>;

return { future, resolve, reject };
return { future, operation: operation.operation, resolve, reject };
}
38 changes: 36 additions & 2 deletions lib/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export function createTask<T>(options: TaskOptions<T>): NewTask<T> {
return Object.defineProperties(Object.create(Promise.prototype), {
[Symbol.iterator]: {
enumerable: false,
value: destroy,
*value() {
yield* assertNonCircular(scope);
yield* destroy();
},
},
then: {
enumerable: false,
Expand All @@ -58,7 +61,10 @@ export function createTask<T>(options: TaskOptions<T>): NewTask<T> {
},
[Symbol.iterator]: {
enumerable: false,
value: future.future[Symbol.iterator],
*value() {
yield* assertNonCircular(scope);
return yield* future.operation;
},
},
[Symbol.toStringTag]: {
enumerable: false,
Expand Down Expand Up @@ -147,3 +153,31 @@ export function* trap<T>(operation: () => Operation<T>): Operation<T> {
}) 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";
}
}
163 changes: 140 additions & 23 deletions test/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,29 +202,6 @@ describe("run()", () => {
await expect(task).rejects.toMatchObject({ message: "boom" });
});

it("can halt itself", async () => {
let task: Task<void> = 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<void> = 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() {
Expand Down Expand Up @@ -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<string> = 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<string> = 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<void>;
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<void>;
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<void>;
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<void>;
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);
});
});
});
Loading