Skip to content

Commit

Permalink
toPromise(actorRef) (#4198)
Browse files Browse the repository at this point in the history
* Add toPromise

* Typecheck

* Just need to fix types

* Revert "Just need to fix types"

This reverts commit 31a24c5.

* Fix types

* Fix tests

* Refactor test

* Add jsdoc

* Tweak things

* Add test to handle errors

* Add tests and immediate resolve/rejection cases

* tweak things

* remove redundant code

* Add changeset

---------

Co-authored-by: Mateusz Burzyński <[email protected]>
  • Loading branch information
davidkpiano and Andarist authored Dec 7, 2023
1 parent c111273 commit ca58904
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 15 deletions.
26 changes: 26 additions & 0 deletions .changeset/calm-nails-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'xstate': minor
---

Introduce `toPromise(actor)`, which creates a promise from an `actor` that resolves with the actor snapshot's `output` when done, or rejects with the actor snapshot's `error` when it fails.

```ts
import { createMachine, createActor, toPromise } from 'xstate';

const machine = createMachine({
// ...
states: {
// ...
done: { type: 'final', output: 42 }
}
});

const actor = createActor(machine);

actor.start();

const output = await toPromise(actor);

console.log(output);
// => 42
```
28 changes: 14 additions & 14 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,31 @@ export * from './actions.ts';
export * from './actors/index.ts';
export { SimulatedClock } from './SimulatedClock.ts';
export { type Spawner } from './spawn.ts';
export { isMachineSnapshot, type MachineSnapshot } from './State.ts';
export { StateMachine } from './StateMachine.ts';
export { getStateNodes } from './stateUtils.ts';
export * from './typegenTypes.ts';
export * from './types.ts';
export { waitFor } from './waitFor.ts';
import { Actor, createActor, interpret, Interpreter } from './interpreter.ts';
import { createMachine } from './createMachine.ts';
export { type MachineSnapshot, isMachineSnapshot } from './State.ts';
import { Actor, createActor, interpret, Interpreter } from './interpreter.ts';
import { StateNode } from './StateNode.ts';
// TODO: decide from where those should be exported
export { and, not, or, stateIn } from './guards.ts';
export { setup } from './setup.ts';
export type {
ActorSystem,
InspectedActorEvent,
InspectedEventEvent,
InspectedSnapshotEvent,
InspectionEvent
} from './system.ts';
export { toPromise } from './toPromise.ts';
export {
getAllOwnEventDescriptors as __unsafe_getAllOwnEventDescriptors,
matchesState,
pathToStateValue,
toObserver,
getAllOwnEventDescriptors as __unsafe_getAllOwnEventDescriptors
toObserver
} from './utils.ts';
export {
Actor,
Expand All @@ -26,16 +36,6 @@ export {
StateNode,
type Interpreter
};
export type {
InspectedActorEvent,
InspectedEventEvent,
InspectedSnapshotEvent,
InspectionEvent,
ActorSystem
} from './system.ts';

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

declare global {
interface SymbolConstructor {
Expand Down
36 changes: 36 additions & 0 deletions packages/core/src/toPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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>> {
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
137 changes: 137 additions & 0 deletions packages/core/test/toPromise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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 () => {
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'));

await expect(toPromise(actor)).rejects.toEqual(new Error('oh noes'));
});
});

0 comments on commit ca58904

Please sign in to comment.