perf: stop recomputing the full client list per client on bulk push

pushAllClients() already has every client profile from its own
listClients() call, but looped through pushClient(key) -> getClient(key)
-> listClients() again for each one - N clients meant N+1 full
order-history recomputations instead of one. pushClient now accepts an
optional already-known profile so the bulk path skips the redundant
lookup; single-key callers (scheduleServerPush's debounce) are
unaffected.

compareClientSources() had the same shape of duplicate work: it called
buildClients()/serverListClients() directly and then again inside
mergeClientSources(). mergeClientSources() now accepts already-computed
local/server arrays instead of always recomputing both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pavlov346346-source 2026-09-12 14:08:57 +03:00
parent 60d00e9f56
commit a08633f9d0

View File

@ -118,19 +118,20 @@
return {key,name:String(row?.name||identity?.name||data?.name||''),phone:String(row?.phone||identity?.phone||data?.phone||''),latestAddress,addresses:latestAddress?[latestAddress]:[],orderIds:[],orderCount:0,totalSpent:0,loyalty:Object.prototype.hasOwnProperty.call(data,'loyalty')?clone(data.loyalty):null,communication:Object.prototype.hasOwnProperty.call(data,'communication')?clone(data.communication):null,serverVersion:Number(row?.version||0)||null,serverUpdatedAt:String(row?.updated_at||''),serverSourceKeys:Array.isArray(row?.source_keys)?row.source_keys.map(String):[],serverSourceCount:Number(row?.source_count||0)||1,canonicalRowPresent:Boolean(row?.canonical_row_present)};
}
function serverListClients(){return currentServerRows().map(serverProfile).filter(Boolean)}
function mergeClientSources(){
const local=buildClients(),byKey=new Map(local.map(item=>[item.key,{...clone(item),dataSource:'local'}]));
for(const server of serverListClients()){
if(server.key.startsWith('o:')&&!byKey.has(server.key))continue;
const current=byKey.get(server.key)||{key:server.key,name:'',phone:'',latestAddress:'',addresses:[],orderIds:[],orderCount:0,totalSpent:0,loyalty:null,communication:null};
const addresses=[...new Set([...(current.addresses||[]),...(server.addresses||[]),server.latestAddress].map(v=>String(v||'').trim()).filter(Boolean))];
byKey.set(server.key,{...current,name:server.name||current.name,phone:server.phone||current.phone,latestAddress:server.latestAddress||current.latestAddress,addresses,loyalty:server.loyalty??current.loyalty,communication:server.communication??current.communication,serverVersion:server.serverVersion,serverUpdatedAt:server.serverUpdatedAt,serverSourceKeys:server.serverSourceKeys,serverSourceCount:server.serverSourceCount,canonicalRowPresent:server.canonicalRowPresent,dataSource:'server+local'});
function mergeClientSources(local,server){
local=local||buildClients();server=server||serverListClients();
const byKey=new Map(local.map(item=>[item.key,{...clone(item),dataSource:'local'}]));
for(const row of server){
if(row.key.startsWith('o:')&&!byKey.has(row.key))continue;
const current=byKey.get(row.key)||{key:row.key,name:'',phone:'',latestAddress:'',addresses:[],orderIds:[],orderCount:0,totalSpent:0,loyalty:null,communication:null};
const addresses=[...new Set([...(current.addresses||[]),...(row.addresses||[]),row.latestAddress].map(v=>String(v||'').trim()).filter(Boolean))];
byKey.set(row.key,{...current,name:row.name||current.name,phone:row.phone||current.phone,latestAddress:row.latestAddress||current.latestAddress,addresses,loyalty:row.loyalty??current.loyalty,communication:row.communication??current.communication,serverVersion:row.serverVersion,serverUpdatedAt:row.serverUpdatedAt,serverSourceKeys:row.serverSourceKeys,serverSourceCount:row.serverSourceCount,canonicalRowPresent:row.canonicalRowPresent,dataSource:'server+local'});
}
return [...byKey.values()].sort((a,b)=>(b.orderCount-a.orderCount)||String(a.name||a.key).localeCompare(String(b.name||b.key),'ru'));
}
function listClients(options={}){const source=typeof options==='string'?options:String(options?.source||'auto');if(source==='local'||source==='legacy')return clone(buildClients());if(source==='server')return clone(serverListClients());return clone(mergeClientSources())}
function compareClientSources(){
const local=buildClients(),server=serverListClients(),merged=mergeClientSources(),serverKeys=new Set(server.map(x=>x.key)),localKeys=new Set(local.map(x=>x.key));
const local=buildClients(),server=serverListClients(),merged=mergeClientSources(local,server),serverKeys=new Set(server.map(x=>x.key)),localKeys=new Set(local.map(x=>x.key));
const metricMismatches=local.map(item=>{const m=merged.find(x=>x.key===item.key);return !m||m.orderCount!==item.orderCount||Number(m.totalSpent)!==Number(item.totalSpent)?item.key:null}).filter(Boolean);
return {localCount:local.length,serverCount:server.length,mergedCount:merged.length,serverPreferred:server.length>0,missingOnServer:[...localKeys].filter(k=>!serverKeys.has(k)),serverOnly:[...serverKeys].filter(k=>!localKeys.has(k)),legacyServerFallbackRows:server.filter(x=>x.key.startsWith('o:')).length,orderMetricsEqual:metricMismatches.length===0,metricMismatches};
}
@ -171,8 +172,8 @@
function setClientLoyalty(key,value,{pushServer=true}={}){writeLegacyMap(LEGACY_LOYALTY_KEY,String(key),value);adoptLegacy({pushServer:false,reason:'client.loyalty'});if(pushServer)scheduleServerPush([String(key)]);return clone(getClient(key)?.loyalty||null)}
function setClientCommunication(key,value,{pushServer=true}={}){writeLegacyMap(LEGACY_COMM_KEY,String(key),value);adoptLegacy({pushServer:false,reason:'client.communication'});if(pushServer)scheduleServerPush([String(key)]);return clone(getClient(key)?.communication||null)}
function serverPayload(profile){return {schemaVersion:1,key:profile.key,identity:{name:String(profile.name||''),phone:String(profile.phone||''),latestAddress:String(profile.latestAddress||'')},loyalty:profile.loyalty?clone(profile.loyalty):null,communication:profile.communication?clone(profile.communication):null,source:'data-layer-v1772',updatedAt:new Date().toISOString()}}
async function pushClient(key){
const profile=getClient(key);if(!profile)return {status:'missing',key};if(!isSignedIn())return {status:'local',key};if(isSupportReadOnly()||!canWrite('clients.edit'))return {status:'read-only',key};
async function pushClient(key,knownProfile){
const profile=knownProfile||getClient(key);if(!profile)return {status:'missing',key};if(!isSignedIn())return {status:'local',key};if(isSupportReadOnly()||!canWrite('clients.edit'))return {status:'read-only',key};
const c=cloud()?.getClient?.(),ws=workspace();if(!c?.rpc||!ws?.id)return {status:'offline',key};
const cached=readClientCache()[key]||{},expected=Number(cached.serverVersion||0)||null;
const {data,error}=await c.rpc('sun_v17_save_client_v1772',{p_workspace:ws.id,p_client_key:key,p_profile:serverPayload(profile),p_expected_version:expected,p_client_id:cloud()?.getClientId?.()||'browser'});if(error)throw error;
@ -181,7 +182,7 @@
}
const pendingServerKeys=new Set();let serverPushTimer=0;
function scheduleServerPush(keys){for(const key of keys||[])if(key)pendingServerKeys.add(String(key));if(serverPushTimer)return;serverPushTimer=setTimeout(async()=>{serverPushTimer=0;const keys=[...pendingServerKeys];pendingServerKeys.clear();for(const key of keys){try{await pushClient(key)}catch(error){console.warn('[Caterium Clients] server save failed',key,error?.message||error)}}},250)}
async function pushAllClients(){const result=[];for(const profile of listClients()){try{result.push(await pushClient(profile.key))}catch(error){result.push({status:'error',key:profile.key,error:String(error?.message||error)})}}return result}
async function pushAllClients(){const result=[];for(const profile of listClients()){try{result.push(await pushClient(profile.key,profile))}catch(error){result.push({status:'error',key:profile.key,error:String(error?.message||error)})}}return result}
let bridgeSuppressed=false;
function installLegacyClientBridge(){