Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

toPromise(actorRef) #4198

Merged
merged 22 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type {
InspectionEvent,
ActorSystem
} from './system.ts';

export { toPromise } from './toPromise.ts';
export { and, not, or, stateIn } from './guards.ts';
export { setup } from './setup.ts';

Expand Down
47 changes: 47 additions & 0 deletions packages/core/src/toPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { AnyActorRef, OutputFrom } from './types.ts';

/**
* Returns a promise that resolves to the `output` of the actor when it is done.
*
* @example
* ```ts
* const machine = createMachine({
* // ...
* output: {
* count: 42
* }
* });
*
* const actor = createActor(machine);
*
* actor.start();
*
* const output = await toPromise(actor);
*
* console.log(output);
* // logs { count: 42 }
* ```
*/
export function toPromise<T extends AnyActorRef>(
actor: T
): Promise<OutputFrom<T>> {
// TODO: this is typed as `any`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't do much about it as long as we use T extends AnyActorRef pattern. You unwrap here what you provided for the TSnapshot within ActorRef but that's just any (since you are using AnyActorRef)

const currentSnapshot = actor.getSnapshot();

if (currentSnapshot.status === 'done') {
return Promise.resolve(currentSnapshot.output);
}
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved

if (currentSnapshot.status === 'error') {
return Promise.reject(currentSnapshot.error);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interestingly, this one is not redundant right now. I think this is a problem and I even thought that we had some tests related to this (although maybe indirect ones). I'll take a look at this right now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that we'll be able to remove this once this lands: #4570

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved ✅


return new Promise((resolve, reject) => {
actor.subscribe({
complete: () => {
resolve(actor.getSnapshot().output);
},
error: reject
});
});
}
4 changes: 3 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ export type OutputFrom<T> = T extends ActorLogic<
infer _TSystem
>
? (TSnapshot & { status: 'done' })['output']
: never;
: T extends ActorRef<infer TSnapshot, infer _TEvent>
? (TSnapshot & { status: 'done' })['output']
: never;

export type ActionFunction<
TContext extends MachineContext,
Expand Down
142 changes: 142 additions & 0 deletions packages/core/test/toPromise.test.ts
davidkpiano marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { createActor, createMachine, fromPromise, toPromise } from '../src';

describe('toPromise', () => {
it('should be awaitable', async () => {
const promiseActor = createActor(
fromPromise(() => Promise.resolve(42))
).start();

const result = await toPromise(promiseActor);

result satisfies number;

expect(result).toEqual(42);
});

it('should await actors', async () => {
const machine = createMachine({
types: {} as {
output: { count: 42 };
},
initial: 'pending',
states: {
pending: {
on: {
RESOLVE: 'done'
}
},
done: {
type: 'final'
}
},
output: { count: 42 }
});

const actor = createActor(machine).start();

setTimeout(() => {
actor.send({ type: 'RESOLVE' });
}, 1);

const data = await toPromise(actor);

data satisfies { count: number };

expect(data).toEqual({ count: 42 });
});

it('should await already done actors', async () => {
const machine = createMachine({
types: {} as {
output: { count: 42 };
},
initial: 'done',
states: {
done: {
type: 'final'
}
},
output: { count: 42 }
});

const actor = createActor(machine).start();

const data = await toPromise(actor);

data satisfies { count: number };

expect(data).toEqual({ count: 42 });
});

it('should handle errors', async () => {
const machine = createMachine({
initial: 'pending',
states: {
pending: {
on: {
REJECT: {
actions: () => {
throw new Error('oh noes');
}
}
}
}
}
});

const actor = createActor(machine).start();

setTimeout(() => {
actor.send({ type: 'REJECT' });
});

try {
await toPromise(actor);
} catch (err) {
expect(err).toEqual(new Error('oh noes'));
}
});

it('should immediately resolve for a done actor', async () => {
const machine = createMachine({
initial: 'done',
states: {
done: {
type: 'final'
}
},
output: {
count: 100
}
});

const actor = createActor(machine).start();

expect(actor.getSnapshot().status).toBe('done');
expect(actor.getSnapshot().output).toEqual({ count: 100 });

const output = await toPromise(actor);

expect(output).toEqual({ count: 100 });
});

it('should immediately reject for an actor that had an error', async () => {
expect.assertions(3);
const machine = createMachine({
entry: () => {
throw new Error('oh noes');
}
});

const actor = createActor(machine).start();

expect(actor.getSnapshot().status).toBe('error');
expect(actor.getSnapshot().error).toEqual(new Error('oh noes'));

try {
await toPromise(actor);
} catch (err) {
expect(err).toEqual(new Error('oh noes'));
}
});
});