Xstate: Interpreted services using extended machine configs don't respect extended config when `start(state)`

Created on 6 Dec 2019  路  4Comments  路  Source: davidkpiano/xstate

Description

  • When extending machine using withConfig() as recommended in the docs
  • If you define a service to be used in the extended machine
  • Providing state (using State.from) to start the interpreted service using the extended machine's config (interpret(extendedMachine).start(state)) won't reference the options.services service provided using withConfig()

Reproduction
https://codesandbox.io/s/zealous-bhaskara-t7omj

Expected Result
Should see, in console output, promise1 and promise2 for the extended machine's invoked service.

Actual Result
Instead, either no options.services.providedService is invoked, or if the same service key is defined on the ancestor machine, that's used instead of the one provided via withConfig()

Additional context

Co-Author of this issue: @tdozi

bug invalid

All 4 comments

It's not that the wrong service is being called, which is made clear when you log the service the state belongs to: https://codesandbox.io/s/stoic-leaf-gs1kv

This is more of an issue of the service from the restored state not being started, which is probably a duplicate of #332, and not an easy problem to solve, since there are two potential desired behaviors:

  1. Assume the service is already running; don't rerun it
  2. Don't assume anything and rerun the service

Those are two very different but valid behaviors for "restore a service at this state" and a feature might need to be developed to control this behavior, which is on the roadmap: #742

Workaround: for a valid restored state with services (_not_ just State.from("something")), iterate through state.children and manually restart the service, if that's what you want to do.

Thanks for the explanation. This makes sense. I had the assumption that it would have no way of knowing what service had originally produced the state. I updated the design of my machines to get around this.

If I end up making a workaround helper to help with testing, I'll post it here.

If I end up making a workaround helper to help with testing, I'll post it here.

Please do, that would be great!

Unsure if I'm verifying all the scenarios I need to be, and I'd like to clean up the bottom tests as well not to use aTimeout, but here's a run at it that appears to be working:

_beginServiceFromState.ts_

import { Interpreter, State, EventObject } from "xstate";

/**
 * Use this when you want to test a service using
 *  - `service.start(State.from())` or
 *  - `withConfig()`
 *
 * @param service
 * @param stateFrom
 */

const beginServiceFromState = <TContext, TEvent extends EventObject, TSchema>(
  service: Interpreter<TContext, TSchema, TEvent>,
  stateFrom: State<TContext, TEvent, TSchema>
): Interpreter<TContext, TSchema, TEvent> => {
  service.start(stateFrom);

  const _restart = service => {
    service.stop();
    service.start();

    service.children.forEach(child => {
      _restart(child);
    });
  };

  _restart(service);

  return service;
};

export default beginServiceFromState;

_tests_

import { Machine, interpret, State, assign } from "xstate";
import { aTimeout } from "@open-wc/testing";
import beginServiceFromState from "./beginServiceFromState";

const promise1 = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("promise1");
    }, 1);
  });
};

const promise2 = () => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve("promise2");
    }, 1);
  });
};

const machine = Machine(
  {
    id: "machine",
    initial: "idle",
    context: {
      invokedService: ""
    },
    states: {
      idle: {
        on: { ACTIVATE: "service" }
      },
      service: {
        invoke: {
          src: "myService",
          onDone: [
            {
              actions: assign((_ctx, e) => {
                return {
                  invokedService: e.data
                };
              })
            }
          ]
        }
      }
    }
  },
  {
    services: {
      myService: promise1
    }
  }
);

describe("fromStateHelper", () => {
  // https://github.com/davidkpiano/xstate/issues/851
  // Service from the restored state isn't started.
  it("iterates through children and manually restarts the service", done => {
    const machine2 = machine.withConfig({
      services: {
        myService: promise2
      }
    });

    const service2 = interpret(machine2);
    const startingState = State.from("service");

    beginServiceFromState(service2, startingState);

    service2.onTransition(state => {
      if (state.event.type === "done.invoke.myService") {
        expect(state.event.data).to.equal("promise2");
        done();
      }
    });

    service2.send("ACTIVATE");
  });

  it("recursively restarts children services", async () => {
    const abMachine = Machine({
      id: "ab",
      initial: "a",
      states: {
        a: { on: { TOGGLE: "b" } },
        b: { on: { TOGGLE: "a" } }
      }
    });

    const parentMachine = Machine({
      id: "parent",
      initial: "idle",
      states: {
        idle: {
          invoke: [
            { id: "ab1", src: abMachine },
            { id: "ab2", src: abMachine }
          ]
        }
      }
    });

    const service = interpret(parentMachine).start();
    service.state.children["ab1"].send("TOGGLE");
    service.state.children["ab2"].send("TOGGLE");

    await aTimeout(1);
    expect(service.state.children["ab1"].state.value).to.equal("b");
    expect(service.state.children["ab2"].state.value).to.equal("b");

    const stateFromService = interpret(parentMachine).start("idle");

    beginServiceFromState(stateFromService);

    await aTimeout(1);
    expect(stateFromService.state.children["ab1"].state.value).to.equal("a");
    expect(stateFromService.state.children["ab2"].state.value).to.equal("a");
  });
});

If this changes significantly when I get through the testing of some more complex cases, I'll update this response

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dakom picture dakom  路  3Comments

hnordt picture hnordt  路  3Comments

kurtmilam picture kurtmilam  路  3Comments

bradwoods picture bradwoods  路  3Comments

ifokeev picture ifokeev  路  3Comments