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

[v5] System for all actor logic #4055

Merged
merged 5 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
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
Copy link
Contributor

@mellson mellson Jun 3, 2023

Choose a reason for hiding this comment

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

It seems that input and/or system is defined as arguments differently across the fromFunctions. We only need the types right?

If that's the case wdyt about adding a shared interface for the types, something like this:

// Terrible name, please help 😅
interface FromRuntime {
  input: any;
  system: AnyActorSystem;
}

export function fromPromise<T>(
  // Leaving out the arguments as we don't use them
  promiseCreator: ({}: FromRuntime) => PromiseLike<T>
)...

Copy link
Member Author

Choose a reason for hiding this comment

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

Will do this in a follow-up PR, good idea

}: {
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();
});
});
Loading