From 82c51c3f863cd920e5feb5559b1eb9bf2b6a45bf Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 11 Dec 2025 10:27:03 -0500 Subject: [PATCH] Ask pattern POC --- packages/core/test/ask.test.ts | 109 +++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 packages/core/test/ask.test.ts diff --git a/packages/core/test/ask.test.ts b/packages/core/test/ask.test.ts new file mode 100644 index 0000000000..6df4733a99 --- /dev/null +++ b/packages/core/test/ask.test.ts @@ -0,0 +1,109 @@ +import { + Actor, + ActorRef, + AnyActorLogic, + createActor, + enqueueActions, + EventFromLogic, + fromTransition, + setup, + Snapshot +} from '../src'; + +/** Extracts askable events (events with respondTo) from actor logic. */ +type AskableEvents = Extract< + EventFromLogic, + { respondTo: ActorRef } +>; + +/** Extracts the response event type from an askable event. */ +type ResponseOf = TAskEvent extends { + respondTo: ActorRef; +} + ? TResponse + : never; + +/** + * Type-safe ask pattern for XState actors. Sends an event to an actor and waits + * for a response. + */ +function ask( + actor: Actor, + event: Omit, 'respondTo'> +): { + map: ( + responseMapper: (response: ResponseOf>) => TResult + ) => Promise; +} { + type TResponse = ResponseOf>; + + return { + map: (responseMapper: (response: TResponse) => TResult) => { + type ResolveEvent = { type: 'resolve'; value: TResult }; + + const tempActor = createActor( + fromTransition( + (_, e, actorScope) => { + actorScope.emit({ type: 'resolve', value: responseMapper(e) }); + return undefined; + }, + undefined + ) + ).start(); + + const promise = new Promise((res) => { + tempActor.on('resolve', (ev) => { + res(ev.value); + }); + }); + + actor.send({ + ...event, + respondTo: tempActor + } as AskableEvents); + + return promise; + } + }; +} + +describe('ask pattern', () => { + it('should work', async () => { + // Define response event type + type AnswerEvent = { type: 'answer'; answer: number }; + + const machine = setup({ + types: { + events: {} as { + type: 'ask'; + respondTo: ActorRef, AnswerEvent>; + } + } + }).createMachine({ + initial: 'idle', + states: { + idle: { + on: { + ask: { + actions: enqueueActions(({ event, enqueue }) => { + enqueue.sendTo(event.respondTo, { type: 'answer', answer: 42 }); + }) + } + } + } + } + }); + + const actor = createActor(machine).start(); + + const result = await ask(actor, { type: 'ask' }).map( + (response) => response.answer + ); + + result satisfies number; + // @ts-expect-error + result satisfies string; + + expect(result).toEqual(42); + }); +});