Skip to content

Commit

Permalink
Hold event loop during main()
Browse files Browse the repository at this point in the history
When you run the following program:

```js
import { main } from "effection";

await main(function*() {
  yield* suspend();
});
```

You get this error

```
error: Top-level await promise never resolved
```

Because there is literally nothing on the event loop which means that
there is nothing that _could_ ever resolve the promise, and yet we
should still be suspended. Actually, there _is_ some state in that
`main()` registers `SIGINT` handlers, but both Deno and Node say that
`SIGINT` doesn't "count" because the state of the art is to call
process.exit() on interrupt and shoot the process heart. Effection on
the other hand removes the sigint handler, so if that "counted" the
`main()` would hold the process open.

This just holds the process open by installing a `setInterval` that fires
every 2^30 milliseconds (about ten years). It is removed when main is finished.

One thing that has occured to me which I'm not sure about is that `run()`
will still exhibit the same behavior. In other words:

```js
await run(suspend);
```

Will complain that the `run()` promise has not resolved, but that
the event loop is exhausted. That feels a bit asymmenric and suprising
which is not great. We could make `suspend()` itself hold the event
loop with the long interval, although that worries me that every
single `suspend()` operation would install a dummy interval. Is it a
tough look to have hundreds, or perhaps even thousands of dummy intervals?

Another option would be to make any Frame that does not have a
parent (aka root frame) of which there is generally only one, hold the
event loop, until its destruction. That would allow it to work with a
bare `run()`, but not install a `setTimeout` every time a suspend()
operation is encountered. The number of dummy intervals would be equal
to the number of root frames (most of the time one).
  • Loading branch information
cowboyd committed Dec 7, 2023
1 parent c6dfc32 commit 395acfc
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ export async function main(
// this function in the exit context so it can be called anywhere.
yield* ExitContext.set(resolve);

// this will hold the event loop and prevent runtimes such as
// Node and Deno from exiting prematurely.
let interval = setInterval(() => {}, Math.pow(2, 30));

try {
let interrupt = () => resolve({ status: 130, signal: "SIGINT" });
yield* withHost({
Expand Down Expand Up @@ -105,6 +109,8 @@ export async function main(
yield* exit(0);
} catch (error) {
resolve({ status: 1, error });
} finally {
clearInterval(interval);
}
})
);
Expand Down
19 changes: 19 additions & 0 deletions test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,25 @@ describe("main", () => {
expect(status.code).toEqual(1);
});
});

it("works even if suspend is the only operation", async () => {
await run(function* () {
let process = yield* useCommand("deno", {
stdout: "piped",
args: ["run", "test/main/just.suspend.ts"],
});
let stdout = yield* buffer(process.stdout);
yield* detect(stdout, "started");

process.kill("SIGINT");

let status = yield* process.status;

expect(status.code).toBe(130);

yield* detect(stdout, "gracefully stopped");
});
});
});

import type { Operation } from "../lib/types.ts";
Expand Down
11 changes: 11 additions & 0 deletions test/main/just.suspend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { suspend } from "../../lib/instructions.ts";
import { main } from "../../lib/main.ts";

await main(function* () {
console.log("started");
try {
yield* suspend();
} finally {
console.log("gracefully stopped");
}
});

0 comments on commit 395acfc

Please sign in to comment.