Skip to content
Merged
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"words": [
"bahmutov",
"degit",
"errdefer",
"esbuild",
"flowgen",
"octocat",
Expand Down
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,54 @@ const result = flow(async function* () {
});
```

### `errdefer()`

```ts
function* errdefer<Error>(
callback: (error: Error) => void | Promise<void>
): Generator<Errdefer<Error>, void, unknown>;
```

This method is similar to `errdefer` in other languages (eg. [zig](https://ziglang.org/documentation/master/#errdefer)). It allows to cleanup eventual leftovers when a method partially failed. Similar to a `finally` keyword.
It takes the error as parameter if you need it

Example:

```ts
let globalTimeout1: NodeJS.Timeout;
let globalTimeout2: NodeJS.Timeout;

// Two dependency starting a long-living process like a timeout or a database connection
const genLongLivingDependency1 = gen(async function longLivingDependency() {
globalTimeout1 = setTimeout(() => {}, 1000);
return "done";
});

const genLongLivingDependency2 = gen(async function longLivingDependency() {
globalTimeout2 = setTimeout(() => {}, 1000);
return "done";
});

const genFailingDependency = gen(async function failingDependency() {
throw new Error("some failing dependency");
});

async function* main() {
const dependency1 = yield* genLongLivingDependency1();
// this will be called if `main` has a failure somewhere
yield* errdefer(() => clearTimeout(globalTimeout1));

const dependency2 = yield* genLongLivingDependency2();
// this will be called if `main` has a failure somewhere, after the first errdefer
yield* errdefer(() => clearTimeout(globalTimeout2));

// since this is failing, it will call every errdefer callback, evaluated in reverse order
const failingDependency = yield* genFailingDependency();

return [dependency1, dependency2, failingDependency];
}
```

### `never()`

```ts
Expand Down Expand Up @@ -285,7 +333,7 @@ async function* method(a: 1 | 2): AsyncGenerator<Error, number> {
function noop(): AsyncGenerator<never, void>;
```

A noop helper. Can be useful when you want to yield nothing just to please the linter.
A noop helper. Can be useful when you want to yield nothing just to please the linter to get a generator even if you don't really yield.

Example:

Expand Down
57 changes: 57 additions & 0 deletions src/__tests__/errdefer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { deepEqual, equal, ok } from "node:assert/strict";
import { describe, it } from "node:test";
import { errdefer } from "../errdefer.ts";
import { flow, gen, noop } from "../index.ts";

let globalTimeout1: NodeJS.Timeout;
let globalTimeout2: NodeJS.Timeout;

await describe("errdefer()", async () => {
await describe("given two long living dependencies that register a timeout and a failing dependency", async () => {
const genLongLivingDependency1 = gen(async function longLivingDependency() {
globalTimeout1 = setTimeout(() => {}, 1000);
return "done";
});

const genLongLivingDependency2 = gen(async function longLivingDependency() {
globalTimeout2 = setTimeout(() => {}, 1000);
return "done";
});

const genFailingDependency = gen(async function failingDependency() {
throw new Error("some failing dependency");
});

await describe("given a main method that yields the generator and errdefer a cleanup", async () => {
const cleanupOrder: number[] = [];

async function* main() {
const dependency1 = yield* genLongLivingDependency1();
yield* errdefer((error) => {
equal((error as Error).message, "some failing dependency");
cleanupOrder.push(1);
clearTimeout(globalTimeout1);
});

yield* noop();

const dependency2 = yield* genLongLivingDependency2();
yield* errdefer((error) => {
equal((error as Error).message, "some failing dependency");
cleanupOrder.push(2);
clearTimeout(globalTimeout2);
});
const failingDependency = yield* genFailingDependency();

return [dependency1, dependency2, failingDependency];
}

await it("should cleanup, in order", async () => {
const result = await flow(main);
ok(result.ok === false);
equal((result.error as Error).message, "some failing dependency");
deepEqual(cleanupOrder, [1, 2]);
});
});
});
});
19 changes: 19 additions & 0 deletions src/errdefer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export interface Errdefer<Error> {
type: "errdefer";
callback: (error: Error) => void | Promise<void>;
}

export function* errdefer<Error>(
callback: (err: Error) => void | Promise<void>,
): Generator<Errdefer<Error>, void, unknown> {
yield { type: "errdefer", callback };
}

export function isErrdefer<Error>(obj: unknown): obj is Errdefer<Error> {
return (
typeof obj === "object" &&
obj !== null &&
"type" in obj &&
obj.type === "errdefer"
);
}
34 changes: 28 additions & 6 deletions src/flow.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { type Errdefer, isErrdefer } from "./errdefer.ts";

/**
* This method turns a generator into a promise. Useful as entrypoint before using generators.
* Inside the generator, always `yield*` other generators.
Expand All @@ -24,15 +26,35 @@
*
*/
export async function flow<Error, Value>(
generator: () => Generator<Error, Value> | AsyncGenerator<Error, Value>,
generator: () =>
| Generator<Error | Errdefer<Error>, Value>
| AsyncGenerator<Error | Errdefer<Error>, Value>,
): Promise<{ ok: true; value: Value } | { ok: false; error: Error }> {
const step = await generator().next();
const deferQueue: Errdefer<Error>[] = [];

if (step.done === false) {
return { ok: false, error: step.value };
}
const gen = generator();

while (true) {
const step = await gen.next();

if (step.done === true) {
return { ok: true, value: step.value as Value };
}

return { ok: true, value: step.value as Value };
if (isErrdefer(step.value)) {
deferQueue.push(step.value);
continue;
}

const error = step.value as Error;

while (deferQueue.length > 0) {
const defer = deferQueue.shift();
await defer?.callback(error);
}

return { ok: false, error };
}
}

/**
Expand Down