state.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // State management for Pynecone web apps.
  2. import io from 'socket.io-client';
  3. // Global variable to hold the token.
  4. let token;
  5. // Key for the token in the session storage.
  6. const TOKEN_KEY = "token";
  7. /**
  8. * Generate a UUID (Used for session tokens).
  9. * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
  10. * @returns A UUID.
  11. */
  12. const generateUUID = () => {
  13. let d = new Date().getTime(),
  14. d2 = (performance && performance.now && performance.now() * 1000) || 0;
  15. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
  16. let r = Math.random() * 16;
  17. if (d > 0) {
  18. r = (d + r) % 16 | 0;
  19. d = Math.floor(d / 16);
  20. } else {
  21. r = (d2 + r) % 16 | 0;
  22. d2 = Math.floor(d2 / 16);
  23. }
  24. return (c == "x" ? r : (r & 0x7) | 0x8).toString(16);
  25. });
  26. };
  27. /**
  28. * Get the token for the current session.
  29. * @returns The token.
  30. */
  31. export const getToken = () => {
  32. if (token) {
  33. return token;
  34. }
  35. if (window) {
  36. if (!window.sessionStorage.getItem(TOKEN_KEY)) {
  37. window.sessionStorage.setItem(TOKEN_KEY, generateUUID());
  38. }
  39. token = window.sessionStorage.getItem(TOKEN_KEY);
  40. }
  41. return token;
  42. };
  43. /**
  44. * Apply a delta to the state.
  45. * @param state The state to apply the delta to.
  46. * @param delta The delta to apply.
  47. */
  48. export const applyDelta = (state, delta) => {
  49. for (const substate in delta) {
  50. let s = state;
  51. const path = substate.split(".").slice(1);
  52. while (path.length > 0) {
  53. s = s[path.shift()];
  54. }
  55. for (const key in delta[substate]) {
  56. s[key] = delta[substate][key];
  57. }
  58. }
  59. };
  60. /**
  61. * Send an event to the server.
  62. * @param event The event to send.
  63. * @param router The router object.
  64. * @param socket The socket object to send the event on.
  65. *
  66. * @returns True if the event was sent, false if it was handled locally.
  67. */
  68. export const applyEvent = async (event, router, socket) => {
  69. // Handle special events
  70. if (event.name == "_redirect") {
  71. router.push(event.payload.path);
  72. return false;
  73. }
  74. if (event.name == "_console") {
  75. console.log(event.payload.message);
  76. return false;
  77. }
  78. if (event.name == "_alert") {
  79. alert(event.payload.message);
  80. return false;
  81. }
  82. // Send the event to the server.
  83. event.token = getToken();
  84. event.router_data = (({ pathname, query }) => ({ pathname, query }))(router);
  85. if (socket) {
  86. socket.emit("event", JSON.stringify(event));
  87. return true;
  88. }
  89. return false;
  90. };
  91. /**
  92. * Process an event off the event queue.
  93. * @param state The state with the event queue.
  94. * @param setState The function to set the state.
  95. * @param result The current result
  96. * @param setResult The function to set the result.
  97. * @param router The router object.
  98. * @param socket The socket object to send the event on.
  99. */
  100. export const updateState = async (state, setState, result, setResult, router, socket) => {
  101. // If we are already processing an event, or there are no events to process, return.
  102. if (result.processing || state.events.length == 0) {
  103. return;
  104. }
  105. // Set processing to true to block other events from being processed.
  106. setResult({ ...result, processing: true });
  107. // Pop the next event off the queue and apply it.
  108. const event = state.events.shift()
  109. // Set new events to avoid reprocessing the same event.
  110. setState({ ...state, events: state.events });
  111. // Apply the event.
  112. const eventSent = await applyEvent(event, router, socket);
  113. if (!eventSent) {
  114. // If no event was sent, set processing to false and return.
  115. setResult({...state, processing: false})
  116. }
  117. };
  118. /**
  119. * Connect to a websocket and set the handlers.
  120. * @param socket The socket object to connect.
  121. * @param state The state object to apply the deltas to.
  122. * @param setState The function to set the state.
  123. * @param setResult The function to set the result.
  124. * @param endpoint The endpoint to connect to.
  125. */
  126. export const connect = async (socket, state, setState, result, setResult, router, endpoint) => {
  127. // Create the socket.
  128. socket.current = io(endpoint, {
  129. 'path': '/event',
  130. });
  131. // Once the socket is open, hydrate the page.
  132. socket.current.on('connect', () => {
  133. updateState(state, setState, result, setResult, router, socket.current);
  134. });
  135. // On each received message, apply the delta and set the result.
  136. socket.current.on('event', function (update) {
  137. update = JSON.parse(update);
  138. applyDelta(state, update.delta);
  139. setResult({
  140. processing: false,
  141. state: state,
  142. events: update.events,
  143. });
  144. });
  145. };
  146. /**
  147. * Create an event object.
  148. * @param name The name of the event.
  149. * @param payload The payload of the event.
  150. * @returns The event object.
  151. */
  152. export const E = (name, payload) => {
  153. return { name, payload };
  154. };