Skip to main content
Glama
useSubscription.ts7.88 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { SubscriptionEmitter, SubscriptionEventMap } from '@medplum/core'; import { deepEquals } from '@medplum/core'; import type { Bundle, Subscription } from '@medplum/fhirtypes'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useMedplum } from '../MedplumProvider/MedplumProvider.context'; const SUBSCRIPTION_DEBOUNCE_MS = 3000; export type UseSubscriptionOptions = { subscriptionProps?: Partial<Subscription>; onWebSocketOpen?: () => void; onWebSocketClose?: () => void; onSubscriptionConnect?: (subscriptionId: string) => void; onSubscriptionDisconnect?: (subscriptionId: string) => void; onError?: (err: Error) => void; }; /** * Creates an in-memory `Subscription` resource with the given criteria on the Medplum server and calls the given callback when an event notification is triggered by a resource interaction over a WebSocket connection. * * Subscriptions created with this hook are lightweight, share a single WebSocket connection, and are automatically untracked and cleaned up when the containing component is no longer mounted. * * @param criteria - The FHIR search criteria to subscribe to. * @param callback - The callback to call when a notification event `Bundle` for this `Subscription` is received. * @param options - Optional options used to configure the created `Subscription`. See {@link UseSubscriptionOptions} * * -------------------------------------------------------------------------------------------------------------------------------- * * `options` contains the following properties, all of which are optional: * - `subscriptionProps` - Allows the caller to pass a `Partial<Subscription>` to use as part of the creation * of the `Subscription` resource for this subscription. It enables the user namely to pass things like the `extension` property and to create * the `Subscription` with extensions such the {@link https://www.medplum.com/docs/subscriptions/subscription-extensions#interactions | Supported Interaction} extension which would enable to listen for `create` or `update` only events. * - `onWebsocketOpen` - Called when the WebSocket connection is established with Medplum server. * - `onWebsocketClose` - Called when the WebSocket connection disconnects. * - `onSubscriptionConnect` - Called when the corresponding subscription starts to receive updates after the subscription has been initialized and connected to. * - `onSubscriptionDisconnect` - Called when the corresponding subscription is destroyed and stops receiving updates from the server. * - `onError` - Called whenever an error occurs during the lifecycle of the managed subscription. */ export function useSubscription( criteria: string | undefined, callback: (bundle: Bundle) => void, options?: UseSubscriptionOptions ): void { const medplum = useMedplum(); const [emitter, setEmitter] = useState<SubscriptionEmitter>(); // We don't memoize the entire options object since it contains callbacks and if the callbacks change identity, we don't want to trigger a resubscribe to criteria const [memoizedSubProps, setMemoizedSubProps] = useState(options?.subscriptionProps); const listeningRef = useRef(false); const unsubTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined); const prevCriteriaRef = useRef<string | undefined>(undefined); const prevMemoizedSubPropsRef = useRef<UseSubscriptionOptions['subscriptionProps']>(undefined); const callbackRef = useRef<typeof callback>(callback); callbackRef.current = callback; const onWebSocketOpenRef = useRef<UseSubscriptionOptions['onWebSocketOpen']>(options?.onWebSocketOpen); onWebSocketOpenRef.current = options?.onWebSocketOpen; const onWebSocketCloseRef = useRef<UseSubscriptionOptions['onWebSocketClose']>(options?.onWebSocketClose); onWebSocketCloseRef.current = options?.onWebSocketClose; const onSubscriptionConnectRef = useRef<UseSubscriptionOptions['onSubscriptionConnect']>( options?.onSubscriptionConnect ); onSubscriptionConnectRef.current = options?.onSubscriptionConnect; const onSubscriptionDisconnectRef = useRef<UseSubscriptionOptions['onSubscriptionDisconnect']>( options?.onSubscriptionDisconnect ); onSubscriptionDisconnectRef.current = options?.onSubscriptionDisconnect; const onErrorRef = useRef<UseSubscriptionOptions['onError']>(options?.onError); onErrorRef.current = options?.onError; useEffect(() => { // Deep equals checks referential equality first if (!deepEquals(options?.subscriptionProps, memoizedSubProps)) { setMemoizedSubProps(options?.subscriptionProps); } }, [memoizedSubProps, options]); useEffect(() => { if (unsubTimerRef.current) { clearTimeout(unsubTimerRef.current); unsubTimerRef.current = undefined; } let shouldSubscribe = false; if (prevCriteriaRef.current !== criteria || !deepEquals(prevMemoizedSubPropsRef.current, memoizedSubProps)) { shouldSubscribe = true; } if (shouldSubscribe && prevCriteriaRef.current) { medplum.unsubscribeFromCriteria(prevCriteriaRef.current, prevMemoizedSubPropsRef.current); } // Set prev criteria and options to latest after checking them prevCriteriaRef.current = criteria; prevMemoizedSubPropsRef.current = memoizedSubProps; // We do this after as to not immediately trigger re-render if (shouldSubscribe && criteria) { setEmitter(medplum.subscribeToCriteria(criteria, memoizedSubProps)); } else if (!criteria) { setEmitter(undefined); } return () => { unsubTimerRef.current = setTimeout(() => { setEmitter(undefined); if (criteria) { medplum.unsubscribeFromCriteria(criteria, memoizedSubProps); } }, SUBSCRIPTION_DEBOUNCE_MS); }; }, [medplum, criteria, memoizedSubProps]); const emitterCallback = useCallback((event: SubscriptionEventMap['message']) => { callbackRef.current?.(event.payload); }, []); const onWebSocketOpen = useCallback(() => { onWebSocketOpenRef.current?.(); }, []); const onWebSocketClose = useCallback(() => { onWebSocketCloseRef.current?.(); }, []); const onSubscriptionConnect = useCallback((event: SubscriptionEventMap['connect']) => { onSubscriptionConnectRef.current?.(event.payload.subscriptionId); }, []); const onSubscriptionDisconnect = useCallback((event: SubscriptionEventMap['disconnect']) => { onSubscriptionDisconnectRef.current?.(event.payload.subscriptionId); }, []); const onError = useCallback((event: SubscriptionEventMap['error']) => { onErrorRef.current?.(event.payload); }, []); useEffect(() => { if (!emitter) { return () => undefined; } if (!listeningRef.current) { emitter.addEventListener('message', emitterCallback); emitter.addEventListener('open', onWebSocketOpen); emitter.addEventListener('close', onWebSocketClose); emitter.addEventListener('connect', onSubscriptionConnect); emitter.addEventListener('disconnect', onSubscriptionDisconnect); emitter.addEventListener('error', onError); listeningRef.current = true; } return () => { listeningRef.current = false; emitter.removeEventListener('message', emitterCallback); emitter.removeEventListener('open', onWebSocketOpen); emitter.removeEventListener('close', onWebSocketClose); emitter.removeEventListener('connect', onSubscriptionConnect); emitter.removeEventListener('disconnect', onSubscriptionDisconnect); emitter.removeEventListener('error', onError); }; }, [ emitter, emitterCallback, onWebSocketOpen, onWebSocketClose, onSubscriptionConnect, onSubscriptionDisconnect, onError, ]); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server