import { string } from 'alga-js' import refreshTokenHelper from "../../../utils/refreshTokenHelper" import forceLogoutHelper from "../../../utils/forceLogoutHelper" import errorHandlingHelper from "../../../utils/errorHandlingHelper" import { fetchProductsWithStorage } from "../../../utils/productStorageHelper" /** * GET /api/materials/products[?top=&skip=] * * Full product list with storage rows. Historically this was ONE iDempiere call * with `$expand=M_Storage` over all ~3.3k products — the single most expensive * request in the app (see productStorageHelper.ts for why the expand is a * global-lock hazard in idempiere-rest). It ran ~65×/day and was the request * that tipped iDempiere over on 2026-09-10. * * Now: plain product query + paged m_storage join, a short server-side cache so * a burst of identical calls (page reloads, several users) costs one backend * round-trip, and single-flight so concurrent identical calls share it. * Optional `top`/`skip` let callers page instead of pulling everything. */ const RESPONSE_CACHE_TTL_MS = 60_000 const responseCache = new Map() const inflight = new Map>() const SELECT = string.urlEncode('M_Product_ID,M_Product_UU,Name,Value,SKU,mpn,UPC,Weight,IsBOM,AD_Org_ID,Strapi_Product_documentId') function paging(query: any): { top: number | null; skip: number } { const top = Number(query?.top) const skip = Number(query?.skip) return { top: Number.isFinite(top) && top > 0 ? Math.min(Math.floor(top), 5000) : null, skip: Number.isFinite(skip) && skip > 0 ? Math.floor(skip) : 0, } } function cacheKey(event: any, top: number | null, skip: number): string { const c = getCookie(event, 'logship_client_id') || '' const r = getCookie(event, 'logship_role_id') || '' const o = getCookie(event, 'logship_organization_id') || '' return `${c}:${r}:${o}:${top ?? 'all'}:${skip}` } const loadProducts = async (event: any, token: any, top: number | null, skip: number) => { let url = `models/m_product?$select=${SELECT}&$filter=${string.urlEncode('isActive eq true OR isActive eq false')}&$orderby=${string.urlEncode('M_Product_ID desc')}` if (top) url += `&$top=${top}` if (skip) url += `&$skip=${skip}` return await fetchProductsWithStorage(event, token, url) } const handleFunc = async (event: any, authToken: any = null) => { const token = authToken ?? await getTokenHelper(event) const { top, skip } = paging(getQuery(event)) const key = cacheKey(event, top, skip) const hit = responseCache.get(key) if (hit && Date.now() - hit.at < RESPONSE_CACHE_TTL_MS) return hit.data const running = inflight.get(key) if (running) return await running const p = loadProducts(event, token, top, skip) .then((data: any) => { if (data && Array.isArray(data.records)) responseCache.set(key, { at: Date.now(), data }) return data ?? {} }) .finally(() => inflight.delete(key)) inflight.set(key, p) return await p } export default defineEventHandler(async (event) => { let data: any = {} try { data = await handleFunc(event) } catch(err: any) { try { let authToken: any = await refreshTokenHelper(event) data = await handleFunc(event, authToken) } catch(error: any) { data = errorHandlingHelper(err?.data ?? err, error?.data ?? error) forceLogoutHelper(event, data) } } return data })