Skip to content

Commit

Permalink
Add snapshot update feature to promises
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkpiano committed Dec 2, 2024
1 parent 9d44c90 commit 284ce32
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 19 deletions.
73 changes: 55 additions & 18 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,42 @@ import {
ActorRefFromLogic,
AnyActorRef,
EventObject,
MachineContext,
NonReducibleUnknown,
Snapshot
Snapshot,
StateValue
} from '../types.ts';

export type PromiseSnapshot<TOutput, TInput> = Snapshot<TOutput> & {
input: TInput | undefined;
};
export interface PromiseState {
value?: StateValue;
context?: MachineContext;
}

export type PromiseSnapshot<
TOutput,
TInput,
TPromiseState extends PromiseState
> = Snapshot<TOutput> &
TPromiseState & {
input: TInput | undefined;
};

const XSTATE_PROMISE_RESOLVE = 'xstate.promise.resolve';
const XSTATE_PROMISE_REJECT = 'xstate.promise.reject';
const XSTATE_PROMISE_UPDATE = 'xstate.promise.update';

export type PromiseActorLogic<
TOutput,
TInput = unknown,
TEmitted extends EventObject = EventObject
TEmitted extends EventObject = EventObject,
TPromiseState extends PromiseState = {}
> = ActorLogic<
PromiseSnapshot<TOutput, TInput>,
{ type: string; [k: string]: unknown },
PromiseSnapshot<TOutput, TInput, TPromiseState>,
// | { type: string; [k: string]: unknown }
| { type: typeof XSTATE_PROMISE_RESOLVE; data: TOutput }
| { type: typeof XSTATE_PROMISE_REJECT; data: unknown }
| { type: typeof XSTATE_STOP }
| { type: typeof XSTATE_PROMISE_UPDATE; state: TPromiseState },
TInput, // input
AnyActorSystem,
TEmitted // TEmitted
Expand Down Expand Up @@ -61,9 +79,10 @@ export type PromiseActorLogic<
*
* @see {@link fromPromise}
*/
export type PromiseActorRef<TOutput> = ActorRefFromLogic<
PromiseActorLogic<TOutput, unknown>
>;
export type PromiseActorRef<
TOutput,
TPromiseState extends PromiseState = {}
> = ActorRefFromLogic<PromiseActorLogic<TOutput, unknown, any, TPromiseState>>;

const controllerMap = new WeakMap<AnyActorRef, AbortController>();

Expand Down Expand Up @@ -120,26 +139,29 @@ const controllerMap = new WeakMap<AnyActorRef, AbortController>();
export function fromPromise<
TOutput,
TInput = NonReducibleUnknown,
TEmitted extends EventObject = EventObject
TEmitted extends EventObject = EventObject,
TPromiseState extends PromiseState = {}
>(
promiseCreator: ({
input,
system,
self,
signal,
emit
emit,
update
}: {
/** Data that was provided to the promise actor */
input: TInput;
/** The actor system to which the promise actor belongs */
system: AnyActorSystem;
/** The parent actor of the promise actor */
self: PromiseActorRef<TOutput>;
self: PromiseActorRef<TOutput, TPromiseState>;
signal: AbortSignal;
emit: (emitted: TEmitted) => void;
update: (state: TPromiseState) => void;
}) => PromiseLike<TOutput>
): PromiseActorLogic<TOutput, TInput, TEmitted> {
const logic: PromiseActorLogic<TOutput, TInput, TEmitted> = {
): PromiseActorLogic<TOutput, TInput, TEmitted, TPromiseState> {
const logic: PromiseActorLogic<TOutput, TInput, TEmitted, TPromiseState> = {
config: promiseCreator,
transition: (state, event, scope) => {
if (state.status !== 'active') {
Expand All @@ -148,7 +170,7 @@ export function fromPromise<

switch (event.type) {
case XSTATE_PROMISE_RESOLVE: {
const resolvedValue = (event as any).data;
const resolvedValue = event.data;
return {
...state,
status: 'done',
Expand All @@ -160,7 +182,7 @@ export function fromPromise<
return {
...state,
status: 'error',
error: (event as any).data,
error: event.data,
input: undefined
};
case XSTATE_STOP: {
Expand All @@ -171,6 +193,16 @@ export function fromPromise<
input: undefined
};
}
case XSTATE_PROMISE_UPDATE: {
const {
state: { context, value }
} = event;
return {
...state,
context,
value
};
}
default:
return state;
}
Expand All @@ -189,7 +221,12 @@ export function fromPromise<
system,
self,
signal: controller.signal,
emit
emit,
update: (state) =>
self.send({
type: XSTATE_PROMISE_UPDATE,
state
})
})
);

Expand Down
41 changes: 40 additions & 1 deletion packages/core/test/actorLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
createActor,
AnyActorLogic,
Snapshot,
ActorLogic
ActorLogic,
toPromise
} from '../src/index.ts';
import {
fromCallback,
Expand Down Expand Up @@ -464,6 +465,44 @@ describe('promise logic (fromPromise)', () => {
const fn2 = signalListenerList[1];
expect(fn2).toHaveBeenCalled();
});

it('can emit updates', async () => {
const p = fromPromise(async ({ update }) => {
update({ value: 'starting' });
update({ value: 'loading', context: { progress: 0.6 } });
update({ value: 'finished' });
return 'done data';
});

const stuff: Array<{}> = [];

const actor = createActor(p);
actor.subscribe((s) => {
stuff.push(s);
});
actor.start();

const res = await toPromise(actor);

expect(res).toEqual('done data');

expect(stuff).toEqual(
expect.arrayContaining([
expect.objectContaining({}),
expect.objectContaining({ value: 'starting', context: undefined }),
expect.objectContaining({
value: 'loading',
context: { progress: 0.6 }
}),
expect.objectContaining({ value: 'finished', context: undefined }),
expect.objectContaining({
status: 'done',
output: 'done data',
value: 'finished'
})
])
);
});
});

describe('transition function logic (fromTransition)', () => {
Expand Down

0 comments on commit 284ce32

Please sign in to comment.