useSocket.ts 8.57 KB
import { Ref, computed, onUnmounted, unref, watch } from 'vue';
import { WidgetDataType } from '../../palette/hooks/useDataSource';
import { useWebSocket } from '@vueuse/core';
import { useGlobSetting } from '/@/hooks/setting';
import { isShareMode } from '/@/views/sys/share/hook';
import { getJwtToken, getShareJwtToken } from '/@/utils/auth';
import { isNullAndUnDef } from '/@/utils/is';
import {
  ComponentPropsConfigType,
  DataFetchUpdateFn,
  EntityTypeEnum,
  MultipleDataFetchUpdateFn,
  ReceiveGroupMessageType,
  ReceiveMessageType,
  ScopeTypeEnum,
  SubscribeMessageItemType,
  SubscribeMessageType,
} from '../index.type';
import { DataSource } from '../../palette/types';

interface DeviceGroupMapType {
  subscriptionId: number;
  attributes: Set<string>;
  subscriptionGroup: Record<'uuid' | 'attribute', string>[];
}

interface ComponentUpdateFnMapValueType {
  fn: MultipleDataFetchUpdateFn;
  attributes: string[];
}

const parseMessage = (text: string): ReceiveMessageType => {
  try {
    return JSON.parse(text);
  } catch (error) {
    return {} as ReceiveMessageType;
  }
};

class Subscriber {
  subscribeId = 0;

  deviceGroupMap = new Map<string, DeviceGroupMapType>();

  subscriptionMap = new Map<number, string>();

  componentUpdateFnMap = new Map<string, DataFetchUpdateFn>();

  componentGroupUpdateFnMap = new Map<string, ComponentUpdateFnMapValueType[]>();

  getNextSubscribeId() {
    return this.subscribeId++;
  }

  clearSubscriber = () => {
    this.deviceGroupMap.clear();
    this.subscriptionMap.clear();
    this.componentUpdateFnMap.clear();
  };

  addSubscriber = (info: Record<'deviceId' | 'slaveDeviceId' | 'attribute' | 'uuid', string>) => {
    const { deviceId, attribute, uuid } = info;
    if (!this.deviceGroupMap.has(deviceId)) {
      this.deviceGroupMap.set(deviceId, {
        subscriptionId: this.getNextSubscribeId(),
        attributes: new Set(),
        subscriptionGroup: [],
      });
    }

    const groupInfo = this.deviceGroupMap.get(deviceId);
    groupInfo?.attributes.add(attribute);
    groupInfo?.subscriptionGroup.push({ uuid, attribute });

    this.subscriptionMap.set(groupInfo!.subscriptionId, deviceId);
  };

  genBasicMessage(unsubscribe = false) {
    const message = Array.from(this.deviceGroupMap.entries()).map(([deviceId, value]) => {
      const { subscriptionId, attributes } = value;
      return {
        cmdId: subscriptionId,
        entityId: deviceId,
        keys: Array.from(attributes.values()).join(','),
        entityType: EntityTypeEnum.DEVICE,
        scope: ScopeTypeEnum.LATEST_TELEMERY,
        ...(unsubscribe ? { unsubscribe } : {}),
      } as SubscribeMessageItemType;
    });
    return { tsSubCmds: message } as SubscribeMessageType;
  }

  genUnSubscribeMessage() {
    return this.genBasicMessage(true);
  }

  genSubscribeMessage() {
    return this.genBasicMessage();
  }

  getScopeMessage(message: ReceiveMessageType, attribute: string[]) {
    const data = attribute.reduce((prev, next) => {
      return { ...prev, [next]: (message.data || {})[next] || [[]] };
    }, {} as ReceiveMessageType['data']);

    const latestValues = attribute.reduce((prev, next) => {
      return { ...prev, [next]: (message.latestValues || {})[next] || [[]] };
    }, {} as ReceiveMessageType['data']);

    return {
      subscriptionId: message.subscriptionId,
      errorCode: message.errorCode,
      errorMsg: message.errorMsg,
      data,
      latestValues,
    } as ReceiveMessageType;
  }

  getGroupScopeMessage(message: ReceiveMessageType, attribute: string[], deviceId: string) {
    const result = this.getScopeMessage(message, attribute);

    return {
      ...result,
      data: {
        [deviceId]: result.data,
      },
      latestValues: {
        [deviceId]: result.latestValues,
      },
    } as ReceiveGroupMessageType;
  }

  trackUpdate(uuid: string, fn: Fn) {
    if (!uuid || !fn) return;
    this.componentUpdateFnMap.set(uuid, fn);
  }

  trackUpdateGroup(deviceId: string, data: ComponentUpdateFnMapValueType) {
    if (!deviceId || !data) return;
    if (!this.componentGroupUpdateFnMap.has(deviceId))
      this.componentGroupUpdateFnMap.set(deviceId, []);
    const temp = this.componentGroupUpdateFnMap.get(deviceId);
    temp?.push(data);
  }

  triggerUpdate(message: ReceiveMessageType) {
    const { subscriptionId } = message;
    if (isNullAndUnDef(subscriptionId)) return;
    const deviceId = this.subscriptionMap.get(subscriptionId);
    if (!deviceId) return;
    const deviceGroup = this.deviceGroupMap.get(deviceId);
    if (!deviceGroup) return;
    const { subscriptionGroup } = deviceGroup;

    const updateGroups = this.componentGroupUpdateFnMap.get(deviceId);

    if (updateGroups) {
      (updateGroups || []).forEach((item) => {
        const { attributes, fn } = item;
        try {
          if (!fn) return;
          fn?.(this.getGroupScopeMessage(message, attributes, deviceId), deviceId, attributes);
        } catch (error) {
          console.error(`deviceId: ${deviceId}`);
          throw error;
        }
      });
    }

    subscriptionGroup.forEach((item) => {
      const { attribute, uuid } = item;
      const updateFn = this.componentUpdateFnMap.get(uuid);
      try {
        if (!updateFn) return;
        updateFn?.(this.getScopeMessage(message, [attribute]), attribute);
      } catch (error) {
        console.error(`uuid: ${uuid}`);
        throw error;
      }
    });
  }
}

const subscriber = new Subscriber();

export const useSocket = (dataSourceRef: Ref<WidgetDataType[]>) => {
  let initied = false;

  const { socketUrl } = useGlobSetting();

  const token = isShareMode() ? getShareJwtToken() : getJwtToken();

  const server = `${socketUrl}${token}`;

  const { send, data, close } = useWebSocket(server, {
    onMessage() {
      initied = true;
      try {
        const message = parseMessage(unref(data));
        subscriber.triggerUpdate(message);
      } catch (error) {
        throw Error(error as string);
      }
    },
  });

  const initSubscribe = () => {
    subscriber.clearSubscriber();
    unref(dataSourceRef).forEach((item) => {
      item.dataSource.forEach((temp) => {
        const { deviceId, slaveDeviceId, attribute, uuid } = temp;
        subscriber.addSubscriber({ deviceId, slaveDeviceId, attribute, uuid });
      });
    });
  };

  watch(
    () => dataSourceRef.value,
    (value) => {
      if (value.length) {
        if (initied) {
          const message = JSON.stringify(subscriber.genUnSubscribeMessage());
          send(message);
        }

        initSubscribe();

        const message = JSON.stringify(subscriber.genSubscribeMessage());
        send(message);
      }
    }
  );

  onUnmounted(() => {
    close();
  });
};

export const useDataFetch = (
  props: { config: ComponentPropsConfigType },
  updateFn: DataFetchUpdateFn
) => {
  const getBindAttribute = computed(() => {
    const { config } = props;
    const { option } = config as ComponentPropsConfigType<Recordable, DataSource>;
    return option.attribute;
  });

  if (!unref(getBindAttribute)) return;

  const getUUID = computed(() => {
    return props.config.option.uuid;
  });

  watch(
    () => getUUID,
    () => {
      subscriber.trackUpdate(unref(getUUID), updateFn);
    },
    {
      immediate: true,
    }
  );
  return { getUUID, getBindAttribute };
};

export const useMultipleDataFetch = (
  props: { config: ComponentPropsConfigType },
  updateFn: MultipleDataFetchUpdateFn
) => {
  const getDataSourceGroup = computed(() => {
    const { config } = props;
    const { option } = config;
    const { dataSource } = option || {};

    const group: Record<string, DataSource[]> = {};

    dataSource?.forEach((item) => {
      const { deviceId } = item;
      if (group[deviceId]) {
        group[deviceId].push(item);
      } else {
        group[deviceId] = [item];
      }
    });

    return group;
  });

  if (!Object.keys(unref(getDataSourceGroup)).length) return;
  // const getBindAttributes = computed(() => {
  //   const attributes = props.config.option.dataSource?.map((item) => item.attribute);
  //   return [...new Set(attributes)];
  // });

  // if (!unref(getBindAttributes).length) return;

  // const getDeviceId = computed(() => {
  //   return props.config.option.dataSource?.at(0)?.deviceId;
  // });

  watch(
    () => getDataSourceGroup,
    () => {
      Object.keys(unref(getDataSourceGroup)).forEach((key) => {
        const item = unref(getDataSourceGroup)[key];
        const attributes = [...new Set(item.map((item) => item.attribute))];
        subscriber.trackUpdateGroup(key, {
          attributes,
          fn: updateFn,
        });
      });
    },
    {
      immediate: true,
    }
  );

  return {};
};