/ client / src / components / liveProvider.ts
liveProvider.ts
  1  import { BaseKey, LiveEvent, LiveProvider } from "@refinedev/core";
  2  
  3  /**
  4   * A spoolman websocket event.
  5   */
  6  interface Event {
  7    type: "updated" | "deleted" | "added";
  8    resource: "filament" | "spool" | "vendor";
  9    date: string;
 10    payload: {
 11      id: number;
 12      [key: string]: unknown;
 13    };
 14  }
 15  
 16  /**
 17   * Converts an API URL to a WebSocket URL.
 18   * E.g. "https://example.com/api/v1/..." -> "wss://example.com/api/v1/..."
 19   * or "/api/v1/..." -> "ws://example.com/api/v1/..."
 20   * @param apiUrl The API URL to convert
 21   * @returns The WebSocket URL
 22   */
 23  function toWebsocketURL(apiUrl: string) {
 24    if (apiUrl[0] === "/") {
 25      // Relative URL, e.g. "/api/v1/..."
 26  
 27      // Get the current browser URL
 28      const currentURL = window.location.href;
 29  
 30      // Split the URL to separate the protocol, host, and path
 31      const urlParts = currentURL.split("/");
 32      const protocol = urlParts[0];
 33      const host = urlParts[2];
 34  
 35      if (protocol === "https:") {
 36        return `wss://${host}${apiUrl}`;
 37      } else {
 38        return `ws://${host}${apiUrl}`;
 39      }
 40    } else {
 41      // Absolute URL, e.g. "https://example.com/api/v1/..."
 42  
 43      // Replace the protocol with "ws://"
 44      return apiUrl.replace(/^http/, "ws");
 45    }
 46  }
 47  
 48  /**
 49   * Subscribes to a single resource.
 50   * @param apiUrl The API URL
 51   * @param channel The channel name, not really used
 52   * @param resource The resource name
 53   * @param callback The callback to call when the resource is updated
 54   * @param id Specific ID to subscribe to, if any. If not specified, subscribes to all IDs.
 55   * @returns A function to unsubscribe from the resource
 56   */
 57  function subscribeSingle(
 58    apiUrl: string,
 59    channel: string,
 60    resource: string,
 61    callback: (event: LiveEvent) => void,
 62    id?: BaseKey,
 63  ) {
 64    // Verify that WebSockets are supported
 65    if (!("WebSocket" in window)) {
 66      console.warn("WebSockets are not supported in this browser. Live updates will not be available.");
 67      return () => {};
 68    }
 69  
 70    const websocketURL = id ? toWebsocketURL(`${apiUrl}/${resource}/${id}`) : toWebsocketURL(`${apiUrl}/${resource}`);
 71  
 72    const ws = new WebSocket(websocketURL);
 73    ws.onmessage = (message) => {
 74      const data: Event = JSON.parse(message.data);
 75      const type = data.type === "added" ? "created" : data.type;
 76      const date = new Date(data.date);
 77  
 78      const liveEvent: LiveEvent = {
 79        channel: channel,
 80        type: type,
 81        payload: {
 82          data: data.payload,
 83          ids: [data.payload.id],
 84        },
 85        date: date,
 86      };
 87  
 88      callback(liveEvent);
 89    };
 90  
 91    return () => {
 92      ws.close();
 93    };
 94  }
 95  
 96  const liveProvider = (apiUrl: string): LiveProvider => ({
 97    subscribe: ({ channel, params, callback }) => {
 98      const { resource, subscriptionType, id, ids } = params ?? {};
 99  
100      if (!subscriptionType) {
101        throw new Error("[useSubscription]: `subscriptionType` is required in `params`");
102      }
103  
104      if (!resource) {
105        throw new Error("[useSubscription]: `resource` is required in `params`");
106      }
107  
108      let idList: BaseKey[];
109      if (ids) idList = ids;
110      else if (id) idList = [id];
111      else {
112        // No ID specified, subscribe to all IDs
113        return [subscribeSingle(apiUrl, channel, resource, callback)];
114      }
115  
116      return idList.map((id) => {
117        return subscribeSingle(apiUrl, channel, resource, callback, id);
118      });
119    },
120    unsubscribe: (closers: (() => void)[]) => {
121      closers.forEach((fn) => fn());
122    },
123  });
124  
125  export default liveProvider;