From eb7c8b387930644bffadb2fa2fb8546aee09eb88 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Thu, 8 Jun 2023 19:55:53 -0400 Subject: [PATCH] [v5] System for all actor logic (#4055) * Ensure all actors have access to the system * Oops, forgot fromCallback * Split up tests * Add changeset --- .changeset/slow-peas-repair.md | 17 ++++ packages/core/src/actors/callback.ts | 5 +- packages/core/src/actors/observable.ts | 37 ++++++-- packages/core/src/actors/promise.ts | 14 ++- packages/core/src/types.ts | 22 +---- packages/core/test/actorLogic.test.ts | 82 +++++++++++++++++ packages/core/test/system.test.ts | 121 +++++++++++++++++++++++++ 7 files changed, 267 insertions(+), 31 deletions(-) create mode 100644 .changeset/slow-peas-repair.md diff --git a/.changeset/slow-peas-repair.md b/.changeset/slow-peas-repair.md new file mode 100644 index 0000000000..7a6430ab12 --- /dev/null +++ b/.changeset/slow-peas-repair.md @@ -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 }) => { ... }); +``` diff --git a/packages/core/src/actors/callback.ts b/packages/core/src/actors/callback.ts index 0fe0b60cd3..1229ce11b2 100644 --- a/packages/core/src/actors/callback.ts +++ b/packages/core/src/actors/callback.ts @@ -24,7 +24,7 @@ export function fromCallback( 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) { @@ -39,7 +39,8 @@ export function fromCallback( }; state.dispose = invokeCallback(sender, receiver, { - input: state.input + input: state.input, + system }); if (isPromiseLike(state.dispose)) { diff --git a/packages/core/src/actors/observable.ts b/packages/core/src/actors/observable.ts index bc19be9d1d..db901d64f0 100644 --- a/packages/core/src/actors/observable.ts +++ b/packages/core/src/actors/observable.ts @@ -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 { @@ -15,7 +21,13 @@ export type ObservablePersistedState = Omit< // TODO: this likely shouldn't accept TEvent, observable actor doesn't accept external events export function fromObservable( - observableCreator: ({ input }: { input: any }) => Subscribable + observableCreator: ({ + input, + system + }: { + input: any; + system: AnyActorSystem; + }) => Subscribable ): ActorLogic< TEvent, T | undefined, @@ -88,12 +100,15 @@ export function fromObservable( 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 }); }, @@ -131,7 +146,12 @@ export function fromObservable( */ export function fromEventObservable( - lazyObservable: ({ input }: { input: any }) => Subscribable + lazyObservable: ({ + input + }: { + input: any; + system: AnyActorSystem; + }) => Subscribable ): ActorLogic< EventObject, T | undefined, @@ -190,13 +210,16 @@ export function fromEventObservable( 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); }, diff --git a/packages/core/src/actors/promise.ts b/packages/core/src/actors/promise.ts index d717a829b8..567dadda3d 100644 --- a/packages/core/src/actors/promise.ts +++ b/packages/core/src/actors/promise.ts @@ -1,4 +1,4 @@ -import { ActorLogic } from '../types'; +import { ActorLogic, AnyActorSystem } from '../types'; import { stopSignalType } from '../actors'; export interface PromiseInternalState { @@ -9,7 +9,13 @@ export interface PromiseInternalState { export function fromPromise( // TODO: add types - promiseCreator: ({ input }: { input: any }) => PromiseLike + promiseCreator: ({ + input, + system + }: { + input: any; + system: AnyActorSystem; + }) => PromiseLike ): ActorLogic<{ type: string }, T | undefined, PromiseInternalState> { const resolveEventType = '$$xstate.resolve'; const rejectEventType = '$$xstate.reject'; @@ -58,7 +64,7 @@ export function fromPromise( 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') { @@ -66,7 +72,7 @@ export function fromPromise( } const resolvedPromise = Promise.resolve( - promiseCreator({ input: state.input }) + promiseCreator({ input: state.input, system }) ); resolvedPromise.then( diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e1d169a05f..9f66b0b9d8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -376,26 +376,9 @@ export type InvokeCallback< > = ( sendBack: (event: TSentEvent) => void, onReceive: Receiver, - { input }: { input: any } + { input, system }: { input: any; system: AnyActorSystem } ) => (() => void) | Promise | 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; @@ -1911,6 +1894,9 @@ export interface ActorSystem { _set: (key: K, actorRef: T['actors'][K]) => void; get: (key: K) => T['actors'][K] | undefined; } + +export type AnyActorSystem = ActorSystem; + export type PersistedMachineState = Pick< TState, 'value' | 'output' | 'context' | 'event' | 'done' | 'historyValue' diff --git a/packages/core/test/actorLogic.test.ts b/packages/core/test/actorLogic.test.ts index 10a5e46aa8..0b4d01c236 100644 --- a/packages/core/test/actorLogic.test.ts +++ b/packages/core/test/actorLogic.test.ts @@ -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 @@ -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)', () => { @@ -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)', () => { @@ -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', () => { @@ -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(); + }); }); diff --git a/packages/core/test/system.test.ts b/packages/core/test/system.test.ts index 87451a8ba5..c4486b2c83 100644 --- a/packages/core/test/system.test.ts +++ b/packages/core/test/system.test.ts @@ -1,9 +1,14 @@ +import { of } from 'rxjs'; import { fromCallback } from '../src/actors/callback.ts'; import { ActorRef, ActorSystem, assign, createMachine, + fromEventObservable, + fromObservable, + fromPromise, + fromTransition, interpret, sendTo, stop @@ -269,4 +274,120 @@ describe('system', () => { interpret(machine).start(); }); + + it('should be accessible in promise logic', () => { + expect.assertions(2); + const machine = createMachine({ + invoke: [ + { + src: createMachine({}), + systemId: 'test' + }, + { + src: fromPromise(({ system }) => { + expect(system.get('test')).toBeDefined(); + return Promise.resolve(); + }) + } + ] + }); + + const actor = interpret(machine).start(); + + expect(actor.system.get('test')).toBeDefined(); + }); + + it('should be accessible in transition logic', () => { + expect.assertions(2); + const machine = createMachine({ + invoke: [ + { + src: createMachine({}), + systemId: 'test' + }, + + { + src: fromTransition((_state, _event, { system }) => { + expect(system.get('test')).toBeDefined(); + return 0; + }, 0), + systemId: 'reducer' + } + ] + }); + + const actor = interpret(machine).start(); + + expect(actor.system.get('test')).toBeDefined(); + + // The assertion won't be checked until the transition function gets an event + actor.system.get('reducer')!.send({ type: 'a' }); + }); + + it('should be accessible in observable logic', () => { + expect.assertions(2); + const machine = createMachine({ + invoke: [ + { + src: createMachine({}), + systemId: 'test' + }, + + { + src: fromObservable(({ system }) => { + expect(system.get('test')).toBeDefined(); + return of(0); + }) + } + ] + }); + + const actor = interpret(machine).start(); + + expect(actor.system.get('test')).toBeDefined(); + }); + + it('should be accessible in event observable logic', () => { + expect.assertions(2); + const machine = createMachine({ + invoke: [ + { + src: createMachine({}), + systemId: 'test' + }, + + { + src: fromEventObservable(({ system }) => { + expect(system.get('test')).toBeDefined(); + return of({ type: 'a' }); + }) + } + ] + }); + + const actor = interpret(machine).start(); + + expect(actor.system.get('test')).toBeDefined(); + }); + + it('should be accessible in callback logic', () => { + expect.assertions(2); + const machine = createMachine({ + invoke: [ + { + src: createMachine({}), + systemId: 'test' + }, + { + src: fromCallback((_sendBack, _receive, { system }) => { + expect(system.get('test')).toBeDefined(); + }) + } + ] + }); + + const actor = interpret(machine).start(); + + expect(actor.system.get('test')).toBeDefined(); + }); });