1import { createMachine, assign } from 'xstate';
2
3export enum UpdatesStateMachineEventTypes {
4  CHECK = 'check',
5  CHECK_COMPLETE_AVAILABLE = 'checkCompleteAvailable',
6  CHECK_COMPLETE_UNAVAILABLE = 'checkCompleteUnavailable',
7  CHECK_ERROR = 'checkError',
8  DOWNLOAD = 'download',
9  DOWNLOAD_COMPLETE = 'downloadComplete',
10  DOWNLOAD_ERROR = 'downloadError',
11  RESTART = 'restart',
12}
13
14/**
15 * Simplified model for an update manifest
16 */
17export type Manifest = {
18  updateId: string;
19};
20
21/**
22 * Model for an update event
23 */
24export type UpdatesStateMachineEvent = {
25  type: UpdatesStateMachineEventTypes;
26  body: {
27    message?: string;
28    manifest?: Manifest;
29    isRollBackToEmbedded?: boolean;
30  };
31};
32
33/**
34 * The context structure
35 */
36export interface UpdatesStateMachineContext {
37  isUpdateAvailable: boolean;
38  isUpdatePending: boolean;
39  latestManifest?: Manifest;
40  isChecking: boolean;
41  isDownloading: boolean;
42  isRollback: boolean;
43  downloadedManifest?: Manifest;
44  checkError?: Error;
45  downloadError?: Error;
46}
47
48/**
49 * Actions that modify the context
50 */
51const checkCompleteAvailableAction = assign({
52  latestManifest: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
53    event.body?.manifest || undefined,
54  checkError: () => undefined,
55  isChecking: () => false,
56  isUpdateAvailable: () => true,
57  isRollback: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
58    Boolean(event.body?.isRollBackToEmbedded),
59});
60
61const checkCompleteUnavailableAction = assign({
62  latestManifest: () => undefined,
63  checkError: () => undefined,
64  isChecking: () => false,
65  isUpdateAvailable: () => false,
66  isRollback: () => false,
67});
68
69const checkErrorAction = assign({
70  isChecking: () => false,
71  checkError: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
72    new Error(event.body?.message || 'checkError'),
73});
74
75const downloadCompleteAction = assign({
76  downloadedManifest: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
77    event.body?.manifest || context.downloadedManifest,
78  latestManifest: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
79    event.body?.manifest || context.latestManifest,
80  downloadError: () => undefined,
81  isDownloading: () => false,
82  isUpdatePending: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
83    !!(event.body?.manifest || context.downloadedManifest),
84  isUpdateAvailable: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
85    event.body?.manifest !== undefined || context.isUpdateAvailable,
86});
87
88const downloadErrorAction = assign({
89  downloadError: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
90    new Error(event.body?.message || 'downloadError'),
91  isDownloading: () => false,
92});
93
94const check = assign({
95  isChecking: (context: UpdatesStateMachineContext) => true,
96});
97
98const download = assign({
99  isDownloading: (context: UpdatesStateMachineContext) => true,
100});
101
102/**
103 * Model of the expo-updates state machine, written in Typescript.
104 * The actual implementations of this state machine will be in Swift on iOS and Kotlin on Android.
105 */
106export const UpdatesStateMachine = createMachine<UpdatesStateMachineContext>({
107  id: 'Updates',
108  initial: 'idle',
109  context: {
110    isChecking: false,
111    isDownloading: false,
112    isUpdateAvailable: false,
113    isUpdatePending: false,
114    isRollback: false,
115  },
116  predictableActionArguments: true,
117  states: {
118    idle: {
119      on: {
120        check: {
121          target: 'checking',
122          actions: check,
123        },
124        download: {
125          target: 'downloading',
126          actions: download,
127        },
128        restart: {
129          target: 'restarting',
130        },
131      },
132    },
133    checking: {
134      on: {
135        checkCompleteAvailable: {
136          target: 'idle',
137          actions: [checkCompleteAvailableAction],
138        },
139        checkCompleteUnavailable: {
140          target: 'idle',
141          actions: [checkCompleteUnavailableAction],
142        },
143        checkError: {
144          target: 'idle',
145          actions: [checkErrorAction],
146        },
147      },
148    },
149    downloading: {
150      on: {
151        downloadComplete: {
152          target: 'idle',
153          actions: [downloadCompleteAction],
154        },
155        downloadError: {
156          target: 'idle',
157          actions: [downloadErrorAction],
158        },
159      },
160    },
161    restarting: {
162      type: 'final',
163    },
164  },
165});
166