Skip to content

Commit

Permalink
[v5] System for all actor logic (#4055)
Browse files Browse the repository at this point in the history
* Ensure all actors have access to the system

* Oops, forgot fromCallback

* Split up tests

* Add changeset
  • Loading branch information
davidkpiano committed Jun 8, 2023
1 parent df55ac0 commit eb7c8b3
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 31 deletions.
17 changes: 17 additions & 0 deletions .changeset/slow-peas-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'xstate': major
---

The `system` can now be accessed in all available actor logic creator functions:

```ts
fromPromise(({ system }) => { ... });

fromTransition((state, event, { system }) => { ... });

fromObservable(({ system }) => { ... });

fromEventObservable(({ system }) => { ... });

fromCallback((sendBack, receive, { system }) => { ... });
```
5 changes: 3 additions & 2 deletions packages/core/src/actors/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function fromCallback<TEvent extends EventObject>(
start: (_state, { self }) => {
self.send({ type: startSignalType } as TEvent);
},
transition: (state, event, { self, id }) => {
transition: (state, event, { self, id, system }) => {
if (event.type === startSignalType) {
const sender = (eventForParent: AnyEventObject) => {
if (state.canceled) {
Expand All @@ -39,7 +39,8 @@ export function fromCallback<TEvent extends EventObject>(
};

state.dispose = invokeCallback(sender, receiver, {
input: state.input
input: state.input,
system
});

if (isPromiseLike(state.dispose)) {
Expand Down
37 changes: 30 additions & 7 deletions packages/core/src/actors/observable.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Subscribable, ActorLogic, EventObject, Subscription } from '../types';
import {
Subscribable,
ActorLogic,
EventObject,
Subscription,
AnyActorSystem
} from '../types';
import { stopSignalType } from '../actors';

export interface ObservableInternalState<T> {
Expand All @@ -15,7 +21,13 @@ export type ObservablePersistedState<T> = Omit<

// TODO: this likely shouldn't accept TEvent, observable actor doesn't accept external events
export function fromObservable<T, TEvent extends EventObject>(
observableCreator: ({ input }: { input: any }) => Subscribable<T>
observableCreator: ({
input,
system
}: {
input: any;
system: AnyActorSystem;
}) => Subscribable<T>
): ActorLogic<
TEvent,
T | undefined,
Expand Down Expand Up @@ -88,12 +100,15 @@ export function fromObservable<T, TEvent extends EventObject>(
input
};
},
start: (state, { self }) => {
start: (state, { self, system }) => {
if (state.status === 'done') {
// Do not restart a completed observable
return;
}
state.subscription = observableCreator({ input: state.input }).subscribe({
state.subscription = observableCreator({
input: state.input,
system
}).subscribe({
next: (value) => {
self.send({ type: nextEventType, data: value });
},
Expand Down Expand Up @@ -131,7 +146,12 @@ export function fromObservable<T, TEvent extends EventObject>(
*/

export function fromEventObservable<T extends EventObject>(
lazyObservable: ({ input }: { input: any }) => Subscribable<T>
lazyObservable: ({
input
}: {
input: any;
system: AnyActorSystem;
}) => Subscribable<T>
): ActorLogic<
EventObject,
T | undefined,
Expand Down Expand Up @@ -190,13 +210,16 @@ export function fromEventObservable<T extends EventObject>(
input
};
},
start: (state, { self }) => {
start: (state, { self, system }) => {
if (state.status === 'done') {
// Do not restart a completed observable
return;
}

state.subscription = lazyObservable({ input: state.input }).subscribe({
state.subscription = lazyObservable({
input: state.input,
system
}).subscribe({
next: (value) => {
self._parent?.send(value);
},
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActorLogic } from '../types';
import { ActorLogic, AnyActorSystem } from '../types';
import { stopSignalType } from '../actors';

export interface PromiseInternalState<T> {
Expand All @@ -9,7 +9,13 @@ export interface PromiseInternalState<T> {

export function fromPromise<T>(
// TODO: add types
promiseCreator: ({ input }: { input: any }) => PromiseLike<T>
promiseCreator: ({
input,
system
}: {
input: any;
system: AnyActorSystem;
}) => PromiseLike<T>
): ActorLogic<{ type: string }, T | undefined, PromiseInternalState<T>> {
const resolveEventType = '$$xstate.resolve';
const rejectEventType = '$$xstate.reject';
Expand Down Expand Up @@ -58,15 +64,15 @@ export function fromPromise<T>(
return state;
}
},
start: (state, { self }) => {
start: (state, { self, system }) => {
// TODO: determine how to allow customizing this so that promises
// can be restarted if necessary
if (state.status !== 'active') {
return;
}

const resolvedPromise = Promise.resolve(
promiseCreator({ input: state.input })
promiseCreator({ input: state.input, system })
);

resolvedPromise.then(
Expand Down
22 changes: 4 additions & 18 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,26 +376,9 @@ export type InvokeCallback<
> = (
sendBack: (event: TSentEvent) => void,
onReceive: Receiver<TEvent>,
{ input }: { input: any }
{ input, system }: { input: any; system: AnyActorSystem }
) => (() => void) | Promise<any> | void;

export type ActorLogicCreator<
TContext extends MachineContext,
TEvent extends EventObject,
TActorLogic extends AnyActorLogic = AnyActorLogic
> = (
context: TContext,
event: TEvent,
meta: {
id: string;
data?: any;
src: string;
event: TEvent;
meta: MetaObject | undefined;
input: any;
}
) => TActorLogic;

export interface InvokeMeta {
src: string;
meta: MetaObject | undefined;
Expand Down Expand Up @@ -1911,6 +1894,9 @@ export interface ActorSystem<T extends ActorSystemInfo> {
_set: <K extends keyof T['actors']>(key: K, actorRef: T['actors'][K]) => void;
get: <K extends keyof T['actors']>(key: K) => T['actors'][K] | undefined;
}

export type AnyActorSystem = ActorSystem<any>;

export type PersistedMachineState<TState extends AnyState> = Pick<
TState,
'value' | 'output' | 'context' | 'event' | 'done' | 'historyValue'
Expand Down
82 changes: 82 additions & 0 deletions packages/core/test/actorLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { EMPTY, interval, of, throwError } from 'rxjs';
import { take } from 'rxjs/operators';
import { createMachine, interpret } from '../src/index.ts';
import {
fromCallback,
fromEventObservable,
fromObservable,
fromPromise,
fromTransition
Expand Down Expand Up @@ -191,6 +193,16 @@ describe('promise logic (fromPromise)', () => {
expect(restoredActor.getSnapshot()).toBe(1);
expect(createdPromises).toBe(1);
});

it('should have access to the system', () => {
expect.assertions(1);
const promiseLogic = fromPromise(({ system }) => {
expect(system).toBeDefined();
return Promise.resolve(42);
});

interpret(promiseLogic).start();
});
});

describe('transition function logic (fromTransition)', () => {
Expand Down Expand Up @@ -247,6 +259,18 @@ describe('transition function logic (fromTransition)', () => {

expect(restoredActor.getSnapshot().status).toBe('active');
});

it('should have access to the system', () => {
expect.assertions(1);
const transitionLogic = fromTransition((_state, _event, { system }) => {
expect(system).toBeDefined();
return 42;
}, 0);

const actor = interpret(transitionLogic).start();

actor.send({ type: 'a' });
});
});

describe('observable logic (fromObservable)', () => {
Expand Down Expand Up @@ -321,6 +345,53 @@ describe('observable logic (fromObservable)', () => {

expect(called).toBe(false);
});

it('should have access to the system', () => {
expect.assertions(1);
const observableLogic = fromObservable(({ system }) => {
expect(system).toBeDefined();
return of(42);
});

interpret(observableLogic).start();
});
});

describe('eventObservable logic (fromEventObservable)', () => {
it('should have access to the system', () => {
expect.assertions(1);
const observableLogic = fromEventObservable(({ system }) => {
expect(system).toBeDefined();
return of({ type: 'a' });
});

interpret(observableLogic).start();
});
});

describe('callback logic (fromCallback)', () => {
it('should interpret a callback', () => {
expect.assertions(1);

const callbackLogic = fromCallback((_, receive) => {
receive((event) => {
expect(event).toEqual({ type: 'a' });
});
});

const actor = interpret(callbackLogic).start();

actor.send({ type: 'a' });
});

it('should have access to the system', () => {
expect.assertions(1);
const callbackLogic = fromCallback((_sendBack, _receive, { system }) => {
expect(system).toBeDefined();
});

interpret(callbackLogic).start();
});
});

describe('machine logic', () => {
Expand Down Expand Up @@ -526,4 +597,15 @@ describe('machine logic', () => {
'inner'
);
});

it('should have access to the system', () => {
expect.assertions(1);
const machine = createMachine({
entry: ({ system }) => {
expect(system).toBeDefined();
}
});

interpret(machine).start();
});
});

0 comments on commit eb7c8b3

Please sign in to comment.