@swan-admin/swan-ai-measurements
Version:
provides ai measurement suggestion
181 lines (162 loc) • 5.88 kB
text/typescript
import axios, { AxiosResponse } from "axios";
import { API_ENDPOINTS, APP_AUTH_BASE_URL, APP_BASE_WEBSOCKET_URL, REQUIRED_MESSAGE } from "./constants.js";
import { checkParameters, getUrl } from "./utils.js";
interface MeasurementRecommendation {
shopDomain: string;
scanId: string;
productName: string;
}
interface Callbacks {
onError?: (error: any) => void;
onSuccess?: (data: any) => void;
onClose?: () => void;
onOpen?: () => void;
}
interface MeasurementSocketOptions extends Callbacks {
scanId: string;
}
interface FaceScanSocketOptions extends Callbacks {
faceScanId: string;
}
interface HandleSocket extends Callbacks {
isFallback: boolean;
scanId?: string;
faceScanId?: string;
paramsKey?: string;
delay: number;
}
interface GetMeasurementsCheckOptions {
scanId: string;
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
}
interface HandlePollingOptions {
scanId: string;
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
}
interface HandleTimeOutOptions {
scanId: string;
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
}
class Measurement {
#socketRefs: Record<string, WebSocket | null> = {};
#waitingTimers: Record<string, NodeJS.Timeout | null> = {};
#pollingTimers: Record<string, NodeJS.Timeout | null> = {};
#pollingCounts: Record<string, number> = {};
#accessKey: string;
#stagingUrl: boolean;
constructor(accessKey: string, stagingUrl = false) {
this.#accessKey = accessKey;
this.#stagingUrl = stagingUrl;
}
getMeasurementResult(scanId: string): Promise<AxiosResponse<any>> {
if (!checkParameters(scanId)) {
throw new Error(REQUIRED_MESSAGE);
}
const url = `${getUrl({ urlName: APP_AUTH_BASE_URL, stagingUrl: this.#stagingUrl })}/measurements?scanId=${scanId}`;
return axios.get(url, {
headers: { "X-Api-Key": this.#accessKey },
});
}
getMeasurementRecommendation({ scanId, shopDomain, productName }: MeasurementRecommendation): Promise<AxiosResponse<any>> {
if (!checkParameters(scanId, shopDomain, productName)) {
throw new Error(REQUIRED_MESSAGE);
}
return axios.get(`${getUrl({ urlName: APP_AUTH_BASE_URL, stagingUrl: this.#stagingUrl })}${API_ENDPOINTS.RECOMMENDATION}/scan/${scanId}/shop/${shopDomain}/product/${productName}`, {
headers: { "X-Api-Key": this.#accessKey },
});
}
#disconnectSocket(key: string): void {
this.#socketRefs[key]?.close();
this.#socketRefs[key] = null;
if (this.#waitingTimers[key]) {
clearTimeout(this.#waitingTimers[key]!);
this.#waitingTimers[key] = null;
}
}
#handleTimeOut(options: HandleTimeOutOptions, key: string): void {
const { scanId, onSuccess, onError } = options;
this.#pollingCounts[key] = 1;
this.#waitingTimers[key] = setTimeout(() => {
this.#handlePolling({ scanId, onSuccess, onError }, key);
this.#disconnectSocket(key);
}, 2 * 60000);
}
#handlePolling(options: HandlePollingOptions, key: string): void {
const { scanId, onSuccess, onError } = options;
if (this.#pollingTimers[key]) {
clearTimeout(this.#pollingTimers[key]!);
}
this.#pollingTimers[key] = setTimeout(() => {
this.#getMeasurementsCheck({ scanId, onSuccess, onError }, key);
}, (this.#pollingCounts[key] || 1) * 5000);
}
async #getMeasurementsCheck(options: GetMeasurementsCheckOptions, key: string): Promise<void> {
const { scanId, onSuccess, onError } = options;
try {
const res = await this.getMeasurementResult(scanId);
if (res?.data && res?.data?.isMeasured === true) {
onSuccess?.(res.data);
clearInterval(this.#pollingTimers[key]!);
} else {
if ((this.#pollingCounts[key] || 1) < 8) {
this.#pollingCounts[key] = (this.#pollingCounts[key] || 1) + 1;
this.#handlePolling({ scanId, onSuccess, onError }, key);
} else {
this.#pollingCounts[key] = 1;
clearInterval(this.#pollingTimers[key]!);
onError?.({ scanStatus: "failed", message: "Scan not found", isMeasured: false });
}
}
} catch (e) {
clearInterval(this.#pollingTimers[key]!);
onError?.(e);
}
}
handleMeasurementSocket(options: MeasurementSocketOptions): void {
const { scanId, onError, onSuccess, onClose, onOpen } = options;
if (!checkParameters(scanId)) {
throw new Error(REQUIRED_MESSAGE);
}
this.#handleSocket({ onOpen, scanId, onSuccess, onError, onClose, paramsKey: "scanId", isFallback: true, delay: 5000 });
}
handlFaceScaneSocket(options: FaceScanSocketOptions): void {
const { faceScanId, onError, onSuccess, onClose, onOpen } = options;
if (!checkParameters(faceScanId)) {
throw new Error(REQUIRED_MESSAGE);
}
this.#handleSocket({ onOpen, faceScanId, onSuccess, onError, onClose, paramsKey: "faceScanId", isFallback: false, delay: 1000 });
}
#handleSocket({ onOpen, isFallback, scanId, onSuccess, onError, onClose, paramsKey, faceScanId, delay }: HandleSocket) {
const key = isFallback ? "measurement" : "faceScan";
setTimeout(() => {
this.#disconnectSocket(key);
const url = `${getUrl({ urlName: APP_BASE_WEBSOCKET_URL, stagingUrl: this.#stagingUrl })}${API_ENDPOINTS.SCANNING}?${paramsKey}=${scanId || faceScanId}`;
const socket = new WebSocket(url);
this.#socketRefs[key] = socket;
socket.onopen = () => {
onOpen?.();
if (isFallback && scanId) {
this.#handleTimeOut({ scanId, onSuccess, onError }, key);
}
};
socket.onmessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data?.code === 200 && data?.scanStatus === "success") {
onSuccess?.(data);
} else {
clearTimeout(this.#waitingTimers[key]!);
onError?.(data);
}
if (data?.code === 200 && data?.scanStatus === "success" && data?.resultType === "final") {
clearTimeout(this.#waitingTimers[key]!);
}
};
socket.onclose = () => onClose?.();
socket.onerror = () => {};
}, delay);
}
}
export default Measurement;