state.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. // State management for Pynecone web apps.
  2. import axios from "axios";
  3. import io from "socket.io-client";
  4. import JSON5 from "json5";
  5. import env from "env.json";
  6. const PINGURL = env.pingUrl
  7. const EVENTURL = env.eventUrl
  8. const UPLOADURL = env.uploadUrl
  9. // Global variable to hold the token.
  10. let token;
  11. // Key for the token in the session storage.
  12. const TOKEN_KEY = "token";
  13. // Dictionary holding component references.
  14. export const refs = {};
  15. /**
  16. * Generate a UUID (Used for session tokens).
  17. * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
  18. * @returns A UUID.
  19. */
  20. const generateUUID = () => {
  21. let d = new Date().getTime(),
  22. d2 = (performance && performance.now && performance.now() * 1000) || 0;
  23. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
  24. let r = Math.random() * 16;
  25. if (d > 0) {
  26. r = (d + r) % 16 | 0;
  27. d = Math.floor(d / 16);
  28. } else {
  29. r = (d2 + r) % 16 | 0;
  30. d2 = Math.floor(d2 / 16);
  31. }
  32. return (c == "x" ? r : (r & 0x7) | 0x8).toString(16);
  33. });
  34. };
  35. /**
  36. * Get the token for the current session.
  37. * @returns The token.
  38. */
  39. export const getToken = () => {
  40. if (token) {
  41. return token;
  42. }
  43. if (window) {
  44. if (!window.sessionStorage.getItem(TOKEN_KEY)) {
  45. window.sessionStorage.setItem(TOKEN_KEY, generateUUID());
  46. }
  47. token = window.sessionStorage.getItem(TOKEN_KEY);
  48. }
  49. return token;
  50. };
  51. /**
  52. * Apply a delta to the state.
  53. * @param state The state to apply the delta to.
  54. * @param delta The delta to apply.
  55. */
  56. export const applyDelta = (state, delta) => {
  57. for (const substate in delta) {
  58. let s = state;
  59. const path = substate.split(".").slice(1);
  60. while (path.length > 0) {
  61. s = s[path.shift()];
  62. }
  63. for (const key in delta[substate]) {
  64. s[key] = delta[substate][key];
  65. }
  66. }
  67. };
  68. /**
  69. * Send an event to the server.
  70. * @param event The event to send.
  71. * @param router The router object.
  72. * @param socket The socket object to send the event on.
  73. *
  74. * @returns True if the event was sent, false if it was handled locally.
  75. */
  76. export const applyEvent = async (event, router, socket) => {
  77. // Handle special events
  78. if (event.name == "_redirect") {
  79. router.push(event.payload.path);
  80. return false;
  81. }
  82. if (event.name == "_console") {
  83. console.log(event.payload.message);
  84. return false;
  85. }
  86. if (event.name == "_alert") {
  87. alert(event.payload.message);
  88. return false;
  89. }
  90. if (event.name == "_set_value") {
  91. const ref =
  92. event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
  93. ref.current.value = event.payload.value;
  94. return false;
  95. }
  96. // Send the event to the server.
  97. event.token = getToken();
  98. event.router_data = (({ pathname, query }) => ({ pathname, query }))(router);
  99. if (socket) {
  100. socket.emit("event", JSON.stringify(event));
  101. return true;
  102. }
  103. return false;
  104. };
  105. /**
  106. * Process an event off the event queue.
  107. * @param queue_event The current event
  108. * @param state The state with the event queue.
  109. * @param setResult The function to set the result.
  110. */
  111. export const applyRestEvent = async (queue_event, state, setResult) => {
  112. if (queue_event.handler == "uploadFiles") {
  113. await uploadFiles(state, setResult, queue_event.name);
  114. }
  115. };
  116. /**
  117. * Process an event off the event queue.
  118. * @param state The state with the event queue.
  119. * @param setState The function to set the state.
  120. * @param result The current result.
  121. * @param setResult The function to set the result.
  122. * @param router The router object.
  123. * @param socket The socket object to send the event on.
  124. */
  125. export const updateState = async (
  126. state,
  127. setState,
  128. result,
  129. setResult,
  130. router,
  131. socket
  132. ) => {
  133. // If we are already processing an event, or there are no events to process, return.
  134. if (result.processing || state.events.length == 0) {
  135. return;
  136. }
  137. // Set processing to true to block other events from being processed.
  138. setResult({ ...result, processing: true });
  139. // Pop the next event off the queue and apply it.
  140. const queue_event = state.events.shift();
  141. // Set new events to avoid reprocessing the same event.
  142. setState({ ...state, events: state.events });
  143. // Process events with handlers via REST and all others via websockets.
  144. if (queue_event.handler) {
  145. await applyRestEvent(queue_event, state, setResult);
  146. } else {
  147. const eventSent = await applyEvent(queue_event, router, socket);
  148. if (!eventSent) {
  149. // If no event was sent, set processing to false and return.
  150. setResult({ ...state, processing: false });
  151. }
  152. }
  153. };
  154. /**
  155. * Connect to a websocket and set the handlers.
  156. * @param socket The socket object to connect.
  157. * @param state The state object to apply the deltas to.
  158. * @param setState The function to set the state.
  159. * @param result The current result.
  160. * @param setResult The function to set the result.
  161. * @param endpoint The endpoint to connect to.
  162. * @param transports The transports to use.
  163. */
  164. export const connect = async (
  165. socket,
  166. state,
  167. setState,
  168. result,
  169. setResult,
  170. router,
  171. transports
  172. ) => {
  173. // Get backend URL object from the endpoint
  174. const endpoint_url = new URL(EVENTURL);
  175. // Create the socket.
  176. socket.current = io(EVENTURL, {
  177. path: endpoint_url["pathname"],
  178. transports: transports,
  179. autoUnref: false,
  180. });
  181. // Once the socket is open, hydrate the page.
  182. socket.current.on("connect", () => {
  183. updateState(state, setState, result, setResult, router, socket.current);
  184. });
  185. // On each received message, apply the delta and set the result.
  186. socket.current.on("event", function (update) {
  187. update = JSON5.parse(update);
  188. applyDelta(state, update.delta);
  189. setResult({
  190. processing: true,
  191. state: state,
  192. events: update.events,
  193. });
  194. });
  195. };
  196. /**
  197. * Upload files to the server.
  198. *
  199. * @param state The state to apply the delta to.
  200. * @param setResult The function to set the result.
  201. * @param handler The handler to use.
  202. * @param endpoint The endpoint to upload to.
  203. */
  204. export const uploadFiles = async (state, setResult, handler) => {
  205. const files = state.files;
  206. // return if there's no file to upload
  207. if (files.length == 0) {
  208. return;
  209. }
  210. const headers = {
  211. "Content-Type": files[0].type,
  212. };
  213. const formdata = new FormData();
  214. // Add the token and handler to the file name.
  215. for (let i = 0; i < files.length; i++) {
  216. formdata.append(
  217. "files",
  218. files[i],
  219. getToken() + ":" + handler + ":" + files[i].name
  220. );
  221. }
  222. // Send the file to the server.
  223. await axios.post(UPLOADURL, formdata, headers).then((response) => {
  224. // Apply the delta and set the result.
  225. const update = response.data;
  226. applyDelta(state, update.delta);
  227. // Set processing to false and return.
  228. setResult({
  229. processing: false,
  230. state: state,
  231. events: update.events,
  232. });
  233. });
  234. };
  235. /**
  236. * Create an event object.
  237. * @param name The name of the event.
  238. * @param payload The payload of the event.
  239. * @param use_websocket Whether the event uses websocket.
  240. * @param handler The client handler to process event.
  241. * @returns The event object.
  242. */
  243. export const E = (name, payload = {}, handler = null) => {
  244. return { name, payload, handler };
  245. };
  246. /***
  247. * Check if a value is truthy in python.
  248. * @param val The value to check.
  249. * @returns True if the value is truthy, false otherwise.
  250. */
  251. export const isTrue = (val) => {
  252. return Array.isArray(val) ? val.length > 0 : !!val;
  253. };
  254. /**
  255. * Prevent the default event.
  256. * @param event
  257. */
  258. export const preventDefault = (event) => {
  259. if (event && event.preventDefault) {
  260. event.preventDefault();
  261. }
  262. };