8

I have extensively read the docs, however, there are still some parts that are not entirely clear, and most have to do with actors/services

I am a little fuzzy on the details of spawned actors' lifecycles.

  • Will calling .stop() on a service with spawned actors also .stop() the spawned actors or they are left hanging?
  • How should I be clearing spawned actors from a machine? Is there a way to access spawned actors in children from inside the service itself? (like from inside an action)

Say I have a machine with an action for adding files. When a file is added a new spawn() is called for it, and the reference is stored to the context. Now, when the machine finishes whatever it's doing, I want to reset the context and clear the children after .stop()ing each one.

To be completely specific here is how I have modelled an upload system which implements the behaviour described above.

In this implementation, whenever the machine returns to the idle state, I am resetting the context and manually .stop()ing each spawned actor. However, the actor services are still hanging around in the .children and I cannot access them from inside the machine actions (adding the 3rd parameter meta to the resetContext doesn't lead to anything with access to the current children).

Regarding .stop()ing the actors and clearing the children and resetting the context, ideally I would like to have a separate action for each, to be run when idle state is entered, however since I cannot seem to find a way to access children, doing it all in one action through the context is the only solution I could think of.

Also, of note that in the official example when a todo is deleted its spawned actor is not .stop()ed which makes me wonder if it's an oversight or is there a reason?

Below, for your convenience, is the code that implements the upload system and the file. The full implementation, which also includes the vizualizer, can be found here:

// upload system machine

const uploadSystemMachine = Machine(
  {
    id: 'upload-system',
    initial: 'idle',
    context: {
      files: [],
      successes: 0,
      failures: 0,
      cancellations: 0,
    },
    states: {
      idle: {
        entry: ['clearSpawnedActors', 'resetContext'],
        on: {
          OPEN: 'active',
        },
      },
      active: {
        initial: 'waitingForFiles',
        states: {
          waitingForFiles: {
            on: {
              CLOSE: {
                target: '#upload-system.idle',
              },
              ADD: {
                actions: 'addFile',
              },
              UPLOAD: {
                target: 'pending',
                cond: 'hasFiles',
              },
            },
          },
          pending: {
            entry: 'startFileUploads',
            on: {
              'FILE.UPLOADED': {
                actions: 'incrementSuccesses',
              },
              'FILE.FAILED': {
                actions: 'incrementFailures',
              },
              'FILE.CANCELLED': {
                actions: 'incrementCancellations',
              },
              CANCEL: {
                actions: 'cancelFileUpload',
              },
              '': {
                target: 'completed',
                cond: 'allSettled',
              },
            },
          },
          completed: {
            type: 'final',
            on: {
              CLOSE: '#upload-system.idle',
            },
          },
        },
      },
    },
    on: {
      KILL: '.idle',
    },
  },
  {
    guards: {
      hasFiles: context => {
        return context.files.length > 0;
      },
      hasNoFiles: context => {
        return context.files.length === 0;
      },
      allSettled: context => {
        return (
          context.files.length === context.successes + context.failures + context.cancellations
        );
      },
    },
    actions: {
      startFileUploads: context => {
        context.files.forEach(actor => {
          actor.send('START');
        });
      },
      addFile: assign({
        files: (context, event) => {
          const newValue = spawn(
            fileMachine.withContext(event.target ? event.target.data : {}),
            context.files.length
          );

          return [...context.files, newValue];
        },
      }),
      resetContext: assign({
        files: context => {
          context.files.forEach(actor => {
            console.log(actor);
            actor.stop();
          });

          return [];
        },
        successes: 0,
        failures: 0,
        cancellations: 0,
      }),
      cancelFileUpload: (context, event) => {
        const fileID = event.data.id;
        for (let i = 0; i < context.files.length; i++) {
          if (context.files[i].id === fileID) {
            context.files[i].send('CANCEL');
            break;
          }
        }
      },
      incrementSuccesses: assign({
        successes: context => {
          return context.successes + 1;
        },
      }),
      incrementFailures: assign({
        failures: context => {
          return context.failures + 1;
        },
      }),
      incrementCancellations: assign({
        cancellations: context => {
          return context.cancellations + 1;
        },
      }),
    },
  }
);
// file machine

const fileMachine = Machine(
  {
    id: 'file',
    initial: 'idle',
    context: null,
    states: {
      idle: {
        on: {
          START: 'pending',
        },
      },
      pending: {
        invoke: {
          id: 'upload',
          src: 'upload',
        },
        on: {
          RESOLVED: 'success',
          REJECTED: 'failed',
          CANCEL: 'cancelled',
        },
      },
      success: {
        type: 'final',
        entry: [
          () => {
            console.log('%centering file success state', 'color: green');
          },
          sendParent('FILE.UPLOADED'),
        ],
      },
      failed: {
        type: 'final',
        entry: [
          () => {
            console.log('%centering file failed state', 'color: red');
          },
          sendParent('FILE.FAILED'),
        ],
      },
      cancelled: {
        type: 'final',
        entry: [
          () => {
            console.log('%centering file cancelled state', 'color: orange');
          },
          sendParent('FILE.CANCELLED'),
        ],
      },
    },
  },
  {
    services: {
      upload: (__context, event) => {
        return callback => {
          let cancelRequest;
          let settled;
          const req = new Promise((resolve, reject) => {
            cancelRequest = reject;
            console.log('Started uploading', event);

            return setTimeout(() => {
              const cond = Math.random() > 0.5;

              if (cond) {
                resolve();
              } else {
                reject();
              }
            }, Math.random() * 5000);
          });

          req
            .then(() => {
              settled = true;
              console.log('%cFile uploaded successfully', 'color: green');
              callback('RESOLVED');
            })
            .catch(() => {
              settled = true;
              console.log('%cFile failed to upload', 'color: red');
              callback('REJECTED');
            });

          return () => {
            if (!settled) {
              console.log('canceling request');
              cancelRequest();
            }
          };
        };
      },
    },
  }
);
Dimitris Karagiannis
  • 8,942
  • 8
  • 38
  • 63

0 Answers0