index.jsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { baseUrl } from './baseUrl';
  2. import { h, createContext } from 'preact';
  3. import { MqttProvider } from './mqtt';
  4. import produce from 'immer';
  5. import { useContext, useEffect, useReducer } from 'preact/hooks';
  6. export const FetchStatus = {
  7. NONE: 'none',
  8. LOADING: 'loading',
  9. LOADED: 'loaded',
  10. ERROR: 'error',
  11. };
  12. const initialState = Object.freeze({
  13. host: baseUrl,
  14. queries: {},
  15. });
  16. const Api = createContext(initialState);
  17. function reducer(state, { type, payload, meta }) {
  18. switch (type) {
  19. case 'REQUEST': {
  20. const { url, fetchId } = payload;
  21. const data = state.queries[url]?.data || null;
  22. return produce(state, (draftState) => {
  23. draftState.queries[url] = { status: FetchStatus.LOADING, data, fetchId };
  24. });
  25. }
  26. case 'RESPONSE': {
  27. const { url, ok, data, fetchId } = payload;
  28. return produce(state, (draftState) => {
  29. draftState.queries[url] = { status: ok ? FetchStatus.LOADED : FetchStatus.ERROR, data, fetchId };
  30. });
  31. }
  32. case 'DELETE': {
  33. const { eventId } = payload;
  34. return produce(state, (draftState) => {
  35. Object.keys(draftState.queries).map(function (url, index) {
  36. // If no url or data has no array length then just return state.
  37. if (!(url in draftState.queries) || !draftState.queries[url].data.length) return state;
  38. //Find the index to remove
  39. const removeIndex = draftState.queries[url].data.map((event) => event.id).indexOf(eventId);
  40. if (removeIndex === -1) return;
  41. // We need to keep track of deleted items, This will be used to calculate "ReachEnd" for auto load new events. Events.jsx
  42. const totDeleted = state.queries[url].deleted || 0;
  43. // Splice the deleted index.
  44. draftState.queries[url].data.splice(removeIndex, 1);
  45. draftState.queries[url].deleted = totDeleted + 1;
  46. });
  47. });
  48. }
  49. default:
  50. return state;
  51. }
  52. }
  53. export function ApiProvider({ children }) {
  54. const [state, dispatch] = useReducer(reducer, initialState);
  55. return (
  56. <Api.Provider value={{ state, dispatch }}>
  57. <MqttWithConfig>{children}</MqttWithConfig>
  58. </Api.Provider>
  59. );
  60. }
  61. function MqttWithConfig({ children }) {
  62. const { data, status } = useConfig();
  63. return status === FetchStatus.LOADED ? <MqttProvider config={data}>{children}</MqttProvider> : children;
  64. }
  65. function shouldFetch(state, url, fetchId = null) {
  66. if ((fetchId && url in state.queries && state.queries[url].fetchId !== fetchId) || !(url in state.queries)) {
  67. return true;
  68. }
  69. const { status } = state.queries[url];
  70. return status !== FetchStatus.LOADING && status !== FetchStatus.LOADED;
  71. }
  72. export function useFetch(url, fetchId) {
  73. const { state, dispatch } = useContext(Api);
  74. useEffect(() => {
  75. if (!shouldFetch(state, url, fetchId)) {
  76. return;
  77. }
  78. async function fetchData() {
  79. await dispatch({ type: 'REQUEST', payload: { url, fetchId } });
  80. const response = await fetch(`${state.host}${url}`);
  81. try {
  82. const data = await response.json();
  83. await dispatch({ type: 'RESPONSE', payload: { url, ok: response.ok, data, fetchId } });
  84. } catch (e) {
  85. await dispatch({ type: 'RESPONSE', payload: { url, ok: false, data: null, fetchId } });
  86. }
  87. }
  88. fetchData();
  89. }, [url, fetchId, state, dispatch]);
  90. if (!(url in state.queries)) {
  91. return { data: null, status: FetchStatus.NONE };
  92. }
  93. const data = state.queries[url].data || null;
  94. const status = state.queries[url].status;
  95. const deleted = state.queries[url].deleted || 0;
  96. return { data, status, deleted };
  97. }
  98. export function useDelete() {
  99. const { dispatch, state } = useContext(Api);
  100. async function deleteEvent(eventId) {
  101. if (!eventId) return { success: false };
  102. const response = await fetch(`${state.host}/api/events/${eventId}`, { method: 'DELETE' });
  103. await dispatch({ type: 'DELETE', payload: { eventId } });
  104. return await (response.status < 300 ? response.json() : { success: true });
  105. }
  106. return deleteEvent;
  107. }
  108. export function useApiHost() {
  109. const { state } = useContext(Api);
  110. return state.host;
  111. }
  112. export function useEvents(searchParams, fetchId) {
  113. const url = `/api/events${searchParams ? `?${searchParams.toString()}` : ''}`;
  114. return useFetch(url, fetchId);
  115. }
  116. export function useEvent(eventId, fetchId) {
  117. const url = `/api/events/${eventId}`;
  118. return useFetch(url, fetchId);
  119. }
  120. export function useRecording(camera, fetchId) {
  121. const url = `/api/${camera}/recordings`;
  122. return useFetch(url, fetchId);
  123. }
  124. export function useConfig(searchParams, fetchId) {
  125. const url = `/api/config${searchParams ? `?${searchParams.toString()}` : ''}`;
  126. return useFetch(url, fetchId);
  127. }
  128. export function useStats(searchParams, fetchId) {
  129. const url = `/api/stats${searchParams ? `?${searchParams.toString()}` : ''}`;
  130. return useFetch(url, fetchId);
  131. }