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