#In the schema, how do I type a spawned callback actor ref that is stored in context

1 messages · Page 1 of 1 (latest)

grizzled echo
#

I did find an example of how to type a stored child machine with ActorRefFrom<typeof childMachine>. But how can I do this with a spawned callback?

grave cobalt
grizzled echo
#

Thanks for replying. Sure. Take this example from the docs:

const counterInterval = (callback, receive) => {
  let count = 0;

  const intervalId = setInterval(() => {
    callback({ type: 'COUNT.UPDATE', count });
    count++;
  }, 1000);

  receive(event => {
    if (event.type === 'INC') {
      count++;
    }
  });

  return () => { clearInterval(intervalId); }
}

const machine = createMachine({
  // ...
  {
    actions: assign({
      counterRef: () => spawn(counterInterval)
    })
  }
  // ...
});

I would like to know how to type context.counterRef in this example. So:

{
  schema: {
    context: {} as {
      counterRef: ????;
    },
  }
}

What should I put in place of ????? Not ActorRefFrom<typeof childMachine>, since it is a callback, not a machine. But I cannot find an example of the type I should use.

gusty valley
#

If that's xstate 4 here are the types for spawn

export declare function spawn<T extends Behavior<any, any>>(entity: T, nameOrOptions?: string | SpawnOptions): ActorRefFrom<T>;
export declare function spawn<TC, TE extends EventObject>(entity: StateMachine<TC, any, TE, any, any, any, any>, nameOrOptions?: string | SpawnOptions): ActorRefFrom<StateMachine<TC, any, TE, any, any, any, any>>;
export declare function spawn(entity: Spawnable, nameOrOptions?: string | SpawnOptions): ActorRef<any>;

Then you have

export declare type Spawnable = AnyStateMachine | PromiseLike<any> | InvokeCallback | InteropObservable<any> | Subscribable<any> | Behavior<any>;

So i'd try:

{
  schema: {
    context: {} as {
      counterRef: ActorRef<any>;
    },
  }
}

You might even be able to narrow it down to ActorRef<TEvent, TSentEvent>:

export declare type InvokeCallback<TEvent extends EventObject = AnyEventObject, TSentEvent extends EventObject = AnyEventObject> = (callback: Sender<TSentEvent>, onReceive: Receiver<TEvent>) => (() => void) | Promise<any> | void;

I hope that helps.

grizzled echo
#

Thanks! I must say, I am not a TypeScript expert and these type declarations are a bit over my head. 🧠🔥 How would this narrowing down work exactly? I tried:

ActorRef<TEvent, TSentEvent>, but I get errors saying that TEvent and TSentEvent are names that cannot be found. Should these be provided through generics? If so, how exactly could I use them in the schema in the previous example?

gusty valley
#

The first time you look at a problem you understand 1% of it, every time you look at it again you understand a little more.
My physics teacher told us in the first lesson. So just go ahead and use VSCodes "[ctrl] + click" jump to declaration. As more as you study the xstate type declarations as more you will understand them.

This seems to work for me. Then callback and recive are strongly typed as well.

type MyEvents = {type: 'INC'} | {type: 'DEC'} | {type: 'RESET'};
type MySentEvents = { type: 'COUNT.UPDATE', count: number};

const counterInterval = (callback: Sender<MySentEvents>, receive: Receiver<MyEvents>) => {
  let count = 0;

  const intervalId = setInterval(() => {
    callback({ type: 'COUNT.UPDATE', count });
    count++;
  }, 1000);

  receive(event => {
    if (event.type === 'INC') {
      count++;
    }
  });

  return () => { clearInterval(intervalId); }
}

const machine = createMachine({
  schema: {
    context: {} as {
      counterRef: ActorRef<MyEvents, MySentEvents>
    },
    events: {} as MyEvents,
  },  
  states: {
    a: {
      entry: assign({
        counterRef: () => spawn(counterInterval)
      })
    }
  }
});
grizzled echo
#

ActorRef<any> works, so that is progress! Looking at your last example now.

#

Ah, ok. This makes more sense to me. So I have to declare the MyEvents and MySentEvents strongly and then use those. Do I understand correctly that TSentEvent is used for events that the Actor can send to its parent?

gusty valley
#

Yes and MyEvents is the union of all events that your parent machine accepts.

Just asking myself if COUNT.UPDATE should be in there as well.

grizzled echo
#

If it is indeed the union of all events the parent accepts, I think it should be? Be I'm sure I'm not sure. 🙂

#

Thanks a bunch for the help. Makes things a lot clearer.

gusty valley
#

Above i mixed up the events that the spawned actor accepts with the ones that the parent accepts. So it's like this:

// events that the callback actor accepts
type MyCounterEvents = { type: 'INC' } | { type: 'DEC' } | { type: 'RESET' };
// events that the callback actor sends
type MyCounterSentEvents = { type: 'COUNT.UPDATE'; count: number };

// events that the parent actor accepts
type MyParentEvents =
  | { type: 'EVENT_1' }
  | { type: 'EVENT_2' }
  | MyCounterSentEvents;

const counterInterval = (
  callback: Sender<MyCounterSentEvents>,
  receive: Receiver<MyCounterEvents>
) => {
  let count = 0;

  const intervalId = setInterval(() => {
    callback({ type: 'COUNT.UPDATE', count });
    count++;
  }, 1000);

  receive(event => {
    if (event.type === 'INC') {
      count++;
    }
  });

  return () => {
    clearInterval(intervalId);
  };
};

const machine = createMachine({
  schema: {
    context: {} as {
      counterRef?: ActorRef<MyCounterEvents, MyCounterSentEvents>;
    },
    events: {} as MyParentEvents,
  },
  context: {},
  initial: 'a',
  states: {
    a: {
      on: {
        EVENT_1: {
          actions: sendTo(context => context.counterRef!, { type: 'INC' }),
        },
      },
      entry: assign({
        counterRef: () => spawn(counterInterval),
      }),
    },
  },
});

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

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