Skip to content

Commit

Permalink
Merge pull request #963 from thefrontside/cl/with-resolvers-v3-backport
Browse files Browse the repository at this point in the history
✨ Backport `withResolvers()` from v4 to v3
  • Loading branch information
cowboyd authored Jan 14, 2025
2 parents 3127200 + 6808ec0 commit e378114
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 14 deletions.
1 change: 1 addition & 0 deletions lib/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./queue.ts";
export * from "./signal.ts";
export * from "./ensure.ts";
export * from "./race.ts";
export * from "./with-resolvers.ts";
84 changes: 84 additions & 0 deletions lib/with-resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Err, Ok } from "./result.ts";
import { action } from "./instructions.ts";
import type { Operation, Result } from "./types.ts";

/**
* The return type of {@link withResolvers}. It contains an operation bundled with
* synchronous functions that determine its outcome.
*/
export interface WithResolvers<T> {
/*
* An {@link Operation} that will either produce a value or raise an
* exception when either `resolve` or `reject` is called. No matter
* how many times this operation is yielded to, it will always
* produce the same effect.
*/
operation: Operation<T>;

/**
* Cause {@link operation} to produce `value`. If either `resolve`
* or`reject` has been called before, this will have no effect.
*
* @param value - the value to produce
*/
resolve(value: T): void;

/**
* Cause {@link operation} to raise `Error`. Any calling operation
* waiting on `operation` will. Yielding to `operation` subsequently
* will also raise the same error. * If either `resolve` or`reject`
* has been called before, this will have no effect.
*
* @param error - the error to raise
*/
reject(error: Error): void;
}

/**
* Create an {link @Operation} and two functions to resolve or reject
* it, corresponding to the two parameters passed to the executor of
* the {@link action} constructor. This is the Effection equivalent of
* [Promise.withResolvers()]{@link
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/withResolvers}
*
* @returns an operation and its resolvers.
*/

export function withResolvers<T>(): WithResolvers<T> {
let continuations = new Set<(result: Result<T>) => void>();
let result: Result<T> | undefined = undefined;

let operation: Operation<T> = action<T>(
(resolve, reject) => {
let settle = (outcome: Result<T>) => {
if (outcome.ok) {
resolve(outcome.value);
} else {
reject(outcome.error);
}
};

if (result) {
settle(result);
return () => {};
} else {
continuations.add(settle);
return () => continuations.delete(settle);
}
},
);

let settle = (outcome: Result<T>) => {
if (!result) {
result = outcome;
}
for (let continuation of continuations) {
continuation(result);
}
};

let resolve = (value: T) => settle(Ok(value));
let reject = (error: Error) => settle(Err(error));

return { operation, resolve, reject };
}
8 changes: 5 additions & 3 deletions test/abort-signal.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { describe, expect, it, mock } from "./suite.ts";
import { describe, expect, it } from "./suite.ts";
import { run, useAbortSignal } from "../mod.ts";

describe("useAbortSignal()", () => {
it("aborts whenever it passes out of scope", async () => {
let abort = mock.fn();
let state = { aborted: false };

let abort = () => state.aborted = true;

let signal = await run(function* () {
let signal = yield* useAbortSignal();
Expand All @@ -12,6 +14,6 @@ describe("useAbortSignal()", () => {
return signal;
});
expect(signal.aborted).toBe(true);
expect(abort).toHaveBeenCalled();
expect(state).toEqual({ aborted: true });
});
});
2 changes: 1 addition & 1 deletion test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,5 +165,5 @@ function* detect(
}
yield* sleep(10);
}
expect(buffer.content).toMatch(text);
expect(buffer.content).toMatch(new RegExp(text));
}
2 changes: 1 addition & 1 deletion test/suite.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "https://deno.land/std@0.163.0/testing/bdd.ts";
export { expect, mock } from "https://deno.land/x/expect@v0.3.0/mod.ts";
export { expect } from "jsr:@std/expect";
export { expectType } from "https://esm.sh/ts-expect@1.3.0?pin=v123";

import {
Expand Down
32 changes: 32 additions & 0 deletions test/with-resolvers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { run, withResolvers } from "../mod.ts";
import { describe, expect, it } from "./suite.ts";

describe("withResolvers()", () => {
it("resolves", async () => {
let { operation, resolve } = withResolvers<string>();
resolve("hello");
await expect(run(() => operation)).resolves.toEqual("hello");
});
it("resolves only once", async () => {
let { operation, resolve, reject } = withResolvers<string>();
resolve("hello");
reject(new Error("boom!"));
resolve("goodbye");
await expect(run(() => operation)).resolves.toEqual("hello");
});
it("rejects", async () => {
let { operation, reject } = withResolvers<string>();
reject(new Error("boom!"));
await expect(run(() => operation)).rejects.toMatchObject({
message: "boom!",
});
});
it("rejects only once", async () => {
let { operation, reject } = withResolvers<string>();
reject(new Error("boom!"));
reject(new Error("bam!"));
await expect(run(() => operation)).rejects.toMatchObject({
message: "boom!",
});
});
});
85 changes: 76 additions & 9 deletions www/docs/async-rosetta-stone.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ know how to do it in Effection.
The congruence between vanilla JavaScript constructs and their Effection
counterparts is reflected in the “Async Rosetta Stone.”

| Async/Await | Effection |
| ---------------- | ----------------- |
| `await` | `yield*` |
| `async function` | `function*` |
| `Promise` | `Operation` |
| `new Promise()` | `action()` |
| `for await` | `for yield* each` |
| `AsyncIterable` | `Stream` |
| `AsyncIterator` | `Subscription` |
| Async/Await | Effection |
| ------------------------- | ----------------- |
| `await` | `yield*` |
| `async function` | `function*` |
| `Promise` | `Operation` |
| `new Promise()` | `action()` |
| `Promise.withResolvers()` | `withResolvers()` |
| `for await` | `for yield* each` |
| `AsyncIterable` | `Stream` |
| `AsyncIterator` | `Subscription` |

## `await` \<=> `yield*`

Expand Down Expand Up @@ -154,6 +155,72 @@ A key difference is that the promise body will be executing immediately, but the
action body is only executed when the action is evaluated. Also, it is executed
anew every time the action is evaluated.

## `Promise.withResolvers()` \<=> `withResolvers()`

Both `Promise` and `Operation` can be constructed ahead of time without needing to begin the process that will resolve it. To do this with
a `Promise`, use the `Promise.withResolvers()` function:

```ts
async function main() {
let { promise, resolve } = Promise.withResolvers();

setTimeout(resolve, 1000);

await promise;

console.log("done!")
}
```

In effection:

```ts
import { withResolvers } from "effection";

function* main() {
let { operation, resolve } = withResolvers();

setTimeout(resolve, 1000);

yield* operation;

console.log("done!");
};
```

## `Promise.withResolvers()` \<=> `withResolvers()`

Both `Promise` and `Operation` can be constructed ahead of time without needing to begin the process that will resolve it. To do this with
a `Promise`, use the `Promise.withResolvers()` function:

```ts
async function main() {
let { promise, resolve } = Promise.withResolvers();

setTimeout(resolve, 1000);

await promise;

console.log("done!")
}
```

In effection:

```ts
import { withResolvers } from "effection";

function* main() {
let { operation, resolve } = withResolvers();

setTimeout(resolve, 1000);

yield* operation;

console.log("done!");
};
```

## `for await` \<=> `for yield* each`

Loop over an AsyncIterable with `for await`:
Expand Down

0 comments on commit e378114

Please sign in to comment.