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;