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 (analogous to what is exposed in @expo/use-updates)
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    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: (event: UpdatesStateMachineEvent) => new Error(event.body?.message || 'checkError'),
72});
73
74const downloadCompleteAction = assign({
75  downloadedManifest: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
76    event.body?.manifest || context.downloadedManifest,
77  latestManifest: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
78    event.body?.manifest || context.latestManifest,
79  downloadError: () => undefined,
80  isDownloading: () => false,
81  isUpdatePending: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
82    !!(event.body?.manifest || context.downloadedManifest),
83  isUpdateAvailable: (context: UpdatesStateMachineContext, event: UpdatesStateMachineEvent) =>
84    event.body?.manifest !== undefined || context.isUpdateAvailable,
85});
86
87const downloadErrorAction = assign({
88  downloadError: (event: UpdatesStateMachineEvent) =>
89    new Error(event.body?.message || 'downloadError'),
90  isDownloading: () => false,
91});
92
93const check = assign({
94  isChecking: (context: UpdatesStateMachineContext) => true,
95});
96
97const download = assign({
98  isDownloading: (context: UpdatesStateMachineContext) => true,
99});
100
101/**
102 * Model of the expo-updates state machine, written in Typescript.
103 * The actual implementations of this state machine will be in Swift on iOS and Kotlin on Android.
104 */
105export const UpdatesStateMachine = createMachine<UpdatesStateMachineContext>({
106  id: 'Updates',
107  initial: 'idle',
108  context: {
109    isChecking: false,
110    isDownloading: false,
111    isUpdateAvailable: false,
112    isUpdatePending: false,
113    isRollback: false,
114  },
115  predictableActionArguments: true,
116  states: {
117    idle: {
118      on: {
119        check: {
120          target: 'checking',
121          actions: check,
122        },
123        download: {
124          target: 'downloading',
125          actions: download,
126        },
127        restart: {
128          target: 'restarting',
129        },
130      },
131    },
132    checking: {
133      on: {
134        checkCompleteAvailable: {
135          target: 'idle',
136          actions: [checkCompleteAvailableAction],
137        },
138        checkCompleteUnavailable: {
139          target: 'idle',
140          actions: [checkCompleteUnavailableAction],
141        },
142        checkError: {
143          target: 'idle',
144          actions: [checkErrorAction],
145        },
146      },
147    },
148    downloading: {
149      on: {
150        downloadComplete: {
151          target: 'idle',
152          actions: [downloadCompleteAction],
153        },
154        downloadError: {
155          target: 'idle',
156          actions: [downloadErrorAction],
157        },
158      },
159    },
160    restarting: {
161      type: 'final',
162    },
163  },
164});
165