From 760263d325ff38f7e4bddfd5201da1b6c96e704e Mon Sep 17 00:00:00 2001 From: heswell Date: Wed, 10 Jan 2024 10:40:56 +0000 Subject: [PATCH] UI table many rows (#1115) * refacor scroll in prep for multi million row scrolling * prepare for vitrual scrolling in big datasets * ignore type issue with ArrayProxy * fix test errors caused by reversion key order in dataSOurce --- .../array-data-source/array-data-source.ts | 14 +- .../vuu-data-remote/src/inlined-worker.js | 2463 ++++++++++++++++- .../vuu-data-remote/test/server-proxy.test.ts | 138 +- .../vuu-data-test/src}/ArrayProxy.ts | 0 vuu-ui/packages/vuu-data-test/src/index.ts | 1 + vuu-ui/packages/vuu-table/src/Table.css | 3 +- vuu-ui/packages/vuu-table/src/Table.tsx | 21 +- vuu-ui/packages/vuu-table/src/index.ts | 1 + .../packages/vuu-table/src/table-dom-utils.ts | 5 +- .../packages/vuu-table/src/useDataSource.ts | 19 +- .../vuu-table/src/useKeyboardNavigation.ts | 33 +- vuu-ui/packages/vuu-table/src/useTable.ts | 29 +- .../packages/vuu-table/src/useTableScroll.ts | 243 +- .../vuu-table/src/useTableViewport.ts | 76 +- .../vuu-table/src/useVirtualViewport.ts | 42 - vuu-ui/packages/vuu-utils/src/keyset.ts | 4 +- vuu-ui/packages/vuu-utils/src/row-utils.ts | 56 +- vuu-ui/packages/vuu-utils/test/keyset.test.js | 8 +- .../src/examples/Table/BigData.examples.tsx | 143 + vuu-ui/showcase/src/examples/Table/index.ts | 1 + .../examples/html/components/BigScrollable.ts | 0 .../showcase/src/examples/utils/ArrayLike.ts | 78 - vuu-ui/showcase/src/examples/utils/index.ts | 2 - 23 files changed, 3019 insertions(+), 361 deletions(-) rename vuu-ui/{showcase/src/examples/utils => packages/vuu-data-test/src}/ArrayProxy.ts (100%) delete mode 100644 vuu-ui/packages/vuu-table/src/useVirtualViewport.ts create mode 100644 vuu-ui/showcase/src/examples/Table/BigData.examples.tsx create mode 100644 vuu-ui/showcase/src/examples/html/components/BigScrollable.ts delete mode 100644 vuu-ui/showcase/src/examples/utils/ArrayLike.ts diff --git a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts index cc46c6747..31a742b9e 100644 --- a/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts +++ b/vuu-ui/packages/vuu-data-local/src/array-data-source/array-data-source.ts @@ -439,9 +439,7 @@ export class ArrayDataSource } set range(range: VuuRange) { - if (range.from !== this.#range.from || range.to !== this.#range.to) { - this.setRange(range); - } + this.setRange(range); } protected delete(row: VuuRowDataItemType[]) { @@ -492,9 +490,13 @@ export class ArrayDataSource }; private setRange(range: VuuRange, forceFullRefresh = false) { - this.#range = range; - this.keys.reset(range); - this.sendRowsToClient(forceFullRefresh); + if (range.from !== this.#range.from || range.to !== this.#range.to) { + this.#range = range; + this.keys.reset(range); + this.sendRowsToClient(forceFullRefresh); + } else if (forceFullRefresh) { + this.sendRowsToClient(forceFullRefresh); + } } sendRowsToClient(forceFullRefresh = false, row?: DataSourceRow) { diff --git a/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js b/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js index 579943856..19e233673 100644 --- a/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js +++ b/vuu-ui/packages/vuu-data-remote/src/inlined-worker.js @@ -1,8 +1,2465 @@ export const workerSourceCode = ` -var ie=(r,e,t)=>{if(!e.has(r))throw TypeError("Cannot "+t)};var m=(r,e,t)=>(ie(r,e,"read from private field"),t?t.call(r):e.get(r)),ae=(r,e,t)=>{if(e.has(r))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(r):e.set(r,t)},ue=(r,e,t,s)=>(ie(r,e,"write to private field"),s?s.call(r,t):e.set(r,t),t);function le(r,e,t=[],s=[]){for(let n=0,o=r.length;n{var e,t;if(((e=globalThis.document)==null?void 0:e.cookie)!==void 0)return(t=globalThis.document.cookie.split("; ").find(s=>s.startsWith(\`\${r}=\`)))==null?void 0:t.split("=")[1]};function G({from:r,to:e},t=0,s=Number.MAX_SAFE_INTEGER){if(t===0)return sr>=e&&r=this.to||tr.type==="connection-status",de=r=>r.type==="connection-metrics";var ge=r=>"viewport"in r;var Qe=["error","warn","info","debug"],Ye=r=>typeof r=="string"&&Qe.includes(r),Ze="error",P=()=>{},Xe="error",{loggingLevel:L=Xe}=et(),b=r=>{let e=L==="debug",t=e||L==="info",s=t||L==="warn",n=s||L==="error",o=t?p=>console.info(\`[\${r}] \${p}\`):P,i=s?p=>console.warn(\`[\${r}] \${p}\`):P,u=e?p=>console.debug(\`[\${r}] \${p}\`):P;return{errorEnabled:n,error:n?p=>console.error(\`[\${r}] \${p}\`):P}};function et(){return typeof loggingSettings<"u"?loggingSettings:{loggingLevel:tt()}}function tt(){let r=ce("vuu-logging-level");return Ye(r)?r:Ze}var{debug:st,debugEnabled:nt}=b("range-monitor"),k=class{constructor(e){this.source=e;this.range={from:0,to:0};this.timestamp=0}isSet(){return this.timestamp!==0}set({from:e,to:t}){let{timestamp:s}=this;if(this.range.from=e,this.range.to=t,this.timestamp=performance.now(),s)nt&&st(\`<\${this.source}> [\${e}-\${t}], \${(this.timestamp-s).toFixed(0)} ms elapsed\`);else return 0}};var O=class{constructor(e){this.keys=new Map,this.free=[],this.nextKeyValue=0,this.reset(e)}next(){return this.free.length>0?this.free.pop():this.nextKeyValue++}reset({from:e,to:t}){this.keys.forEach((n,o)=>{(o=t)&&(this.free.push(n),this.keys.delete(o))});let s=t-e;this.keys.size+this.free.length>s&&(this.free.length=Math.max(0,s-this.keys.size));for(let n=e;nthis.keys.size&&(this.nextKeyValue=this.keys.size)}keyFor(e){let t=this.keys.get(e);if(t===void 0)throw console.log(\`key not found +var __accessCheck = (obj, member, msg) => { + if (!member.has(obj)) + throw TypeError("Cannot " + msg); +}; +var __privateGet = (obj, member, getter) => { + __accessCheck(obj, member, "read from private field"); + return getter ? getter.call(obj) : member.get(obj); +}; +var __privateAdd = (obj, member, value) => { + if (member.has(obj)) + throw TypeError("Cannot add the same private member more than once"); + member instanceof WeakSet ? member.add(obj) : member.set(obj, value); +}; +var __privateSet = (obj, member, value, setter) => { + __accessCheck(obj, member, "write to private field"); + setter ? setter.call(obj, value) : member.set(obj, value); + return value; +}; + +// ../vuu-utils/src/array-utils.ts +function partition(array, test, pass = [], fail = []) { + for (let i = 0, len = array.length; i < len; i++) { + (test(array[i], i) ? pass : fail).push(array[i]); + } + return [pass, fail]; +} + +// ../vuu-utils/src/column-utils.ts +var metadataKeys = { + IDX: 0, + RENDER_IDX: 1, + IS_LEAF: 2, + IS_EXPANDED: 3, + DEPTH: 4, + COUNT: 5, + KEY: 6, + SELECTED: 7, + count: 8, + // TODO following only used in datamodel + PARENT_IDX: "parent_idx", + IDX_POINTER: "idx_pointer", + FILTER_COUNT: "filter_count", + NEXT_FILTER_IDX: "next_filter_idx" +}; +var { DEPTH, IS_LEAF } = metadataKeys; + +// ../vuu-utils/src/cookie-utils.ts +var getCookieValue = (name) => { + var _a, _b; + if (((_a = globalThis.document) == null ? void 0 : _a.cookie) !== void 0) { + return (_b = globalThis.document.cookie.split("; ").find((row) => row.startsWith(\`\${name}=\`))) == null ? void 0 : _b.split("=")[1]; + } +}; + +// ../vuu-utils/src/range-utils.ts +function getFullRange({ from, to }, bufferSize = 0, rowCount = Number.MAX_SAFE_INTEGER) { + if (bufferSize === 0) { + if (rowCount < from) { + return { from: 0, to: 0 }; + } else { + return { from, to: Math.min(to, rowCount) }; + } + } else if (from === 0) { + return { from, to: Math.min(to + bufferSize, rowCount) }; + } else { + const rangeSize = to - from; + const buff = Math.round(bufferSize / 2); + const shortfallBefore = from - buff < 0; + const shortFallAfter = rowCount - (to + buff) < 0; + if (shortfallBefore && shortFallAfter) { + return { from: 0, to: rowCount }; + } else if (shortfallBefore) { + return { from: 0, to: rangeSize + bufferSize }; + } else if (shortFallAfter) { + return { + from: Math.max(0, rowCount - (rangeSize + bufferSize)), + to: rowCount + }; + } else { + return { from: from - buff, to: to + buff }; + } + } +} +var withinRange = (value, { from, to }) => value >= from && value < to; +var WindowRange = class _WindowRange { + constructor(from, to) { + this.from = from; + this.to = to; + } + isWithin(index) { + return withinRange(index, this); + } + //find the overlap of this range and a new one + overlap(from, to) { + return from >= this.to || to < this.from ? [0, 0] : [Math.max(from, this.from), Math.min(to, this.to)]; + } + copy() { + return new _WindowRange(this.from, this.to); + } +}; + +// ../vuu-utils/src/datasource-utils.ts +var isConnectionStatusMessage = (msg) => msg.type === "connection-status"; +var isConnectionQualityMetrics = (msg) => msg.type === "connection-metrics"; +var isViewporttMessage = (msg) => "viewport" in msg; + +// ../vuu-utils/src/logging-utils.ts +var logLevels = ["error", "warn", "info", "debug"]; +var isValidLogLevel = (value) => typeof value === "string" && logLevels.includes(value); +var DEFAULT_LOG_LEVEL = "error"; +var NO_OP = () => void 0; +var DEFAULT_DEBUG_LEVEL = false ? "error" : "info"; +var { loggingLevel = DEFAULT_DEBUG_LEVEL } = getLoggingSettings(); +var logger = (category) => { + const debugEnabled5 = loggingLevel === "debug"; + const infoEnabled5 = debugEnabled5 || loggingLevel === "info"; + const warnEnabled = infoEnabled5 || loggingLevel === "warn"; + const errorEnabled = warnEnabled || loggingLevel === "error"; + const info5 = infoEnabled5 ? (message) => console.info(\`[\${category}] \${message}\`) : NO_OP; + const warn4 = warnEnabled ? (message) => console.warn(\`[\${category}] \${message}\`) : NO_OP; + const debug5 = debugEnabled5 ? (message) => console.debug(\`[\${category}] \${message}\`) : NO_OP; + const error4 = errorEnabled ? (message) => console.error(\`[\${category}] \${message}\`) : NO_OP; + if (false) { + return { + errorEnabled, + error: error4 + }; + } else { + return { + debugEnabled: debugEnabled5, + infoEnabled: infoEnabled5, + warnEnabled, + errorEnabled, + info: info5, + warn: warn4, + debug: debug5, + error: error4 + }; + } +}; +function getLoggingSettings() { + if (typeof loggingSettings !== "undefined") { + return loggingSettings; + } else { + return { + loggingLevel: getLoggingLevelFromCookie() + }; + } +} +function getLoggingLevelFromCookie() { + const value = getCookieValue("vuu-logging-level"); + if (isValidLogLevel(value)) { + return value; + } else { + return DEFAULT_LOG_LEVEL; + } +} + +// ../vuu-utils/src/debug-utils.ts +var { debug, debugEnabled } = logger("range-monitor"); +var RangeMonitor = class { + constructor(source) { + this.source = source; + this.range = { from: 0, to: 0 }; + this.timestamp = 0; + } + isSet() { + return this.timestamp !== 0; + } + set({ from, to }) { + const { timestamp } = this; + this.range.from = from; + this.range.to = to; + this.timestamp = performance.now(); + if (timestamp) { + debugEnabled && debug( + \`<\${this.source}> [\${from}-\${to}], \${(this.timestamp - timestamp).toFixed(0)} ms elapsed\` + ); + } else { + return 0; + } + } +}; + +// ../vuu-utils/src/keyset.ts +var KeySet = class { + constructor(range) { + this.keys = /* @__PURE__ */ new Map(); + this.free = []; + this.nextKeyValue = 0; + this.reset(range); + } + next() { + if (this.free.length > 0) { + return this.free.shift(); + } else { + return this.nextKeyValue++; + } + } + reset({ from, to }) { + this.keys.forEach((keyValue, rowIndex) => { + if (rowIndex < from || rowIndex >= to) { + this.free.push(keyValue); + this.keys.delete(rowIndex); + } + }); + const size = to - from; + if (this.keys.size + this.free.length > size) { + this.free.length = Math.max(0, size - this.keys.size); + } + for (let rowIndex = from; rowIndex < to; rowIndex++) { + if (!this.keys.has(rowIndex)) { + const nextKeyValue = this.next(); + this.keys.set(rowIndex, nextKeyValue); + } + } + if (this.nextKeyValue > this.keys.size) { + this.nextKeyValue = this.keys.size; + } + } + keyFor(rowIndex) { + const key = this.keys.get(rowIndex); + if (key === void 0) { + console.log(\`key not found keys: \${this.toDebugString()} free : \${this.free.join(",")} - \`),Error(\`KeySet, no key found for rowIndex \${e}\`);return t}toDebugString(){return Array.from(this.keys.entries()).map((e,t)=>\`\${e}=>\${t}\`).join(",")}};var{SELECTED:\$t}=\$,w={False:0,True:1,First:2,Last:4};var rt=(r,e)=>e>=r[0]&&e<=r[1],ot=w.True+w.First+w.Last,it=w.True+w.First,at=w.True+w.Last,z=(r,e)=>{for(let t of r)if(typeof t=="number"){if(t===e)return ot}else if(rt(t,e))return e===t[0]?it:e===t[1]?at:w.True;return w.False};var he=r=>{if(r.every(t=>typeof t=="number"))return r;let e=[];for(let t of r)if(typeof t=="number")e.push(t);else for(let s=t[0];s<=t[1];s++)e.push(s);return e};var fe=r=>r.type==="VIEW_PORT_MENU_RESP"&&r.action!==null&&x(r.action.table),x=r=>r!==null&&typeof r=="object"&&"table"in r&&"module"in r?r.table.startsWith("session"):!1;var ut=["VIEW_PORT_MENUS_SELECT_RPC","VIEW_PORT_MENU_TABLE_RPC","VIEW_PORT_MENU_ROW_RPC","VIEW_PORT_MENU_CELL_RPC","VP_EDIT_CELL_RPC","VP_EDIT_ROW_RPC","VP_EDIT_ADD_ROW_RPC","VP_EDIT_DELETE_CELL_RPC","VP_EDIT_DELETE_ROW_RPC","VP_EDIT_SUBMIT_FORM_RPC"],me=r=>ut.includes(r.type),Ce=r=>r.type==="VIEW_PORT_RPC_CALL",A=({requestId:r,...e})=>[r,e],Re=r=>{let e=r.at(0);if(e.updateType==="SIZE"){if(r.length===1)return r;e=r.at(1)}let t=r.at(-1);return[e,t]},Se=r=>{let e={};for(let t of r)(e[t.viewPortId]||(e[t.viewPortId]=[])).push(t);return e},H=({columns:r,dataTypes:e,key:t,table:s})=>({table:s,columns:r.map((n,o)=>({name:n,serverDataType:e[o]})),key:t});var Te="CHANGE_VP_SUCCESS";var be="CLOSE_TREE_NODE",Ee="CLOSE_TREE_SUCCESS";var we="CREATE_VP",Ve="DISABLE_VP",ve="DISABLE_VP_SUCCESS";var ye="ENABLE_VP",Me="ENABLE_VP_SUCCESS";var K="GET_VP_VISUAL_LINKS",_e="GET_VIEW_PORT_MENUS";var Ie="HB",De="HB_RESP",Pe="LOGIN",Le="OPEN_TREE_NODE",ke="OPEN_TREE_SUCCESS";var Oe="REMOVE_VP";var xe="SET_SELECTION_SUCCESS";var Ne=r=>{switch(r){case"TypeAheadRpcHandler":return"TYPEAHEAD";default:return"SIMUL"}};var Ue=[],R=b("array-backed-moving-window");function lt(r,e){if(!e||e.data.length!==r.data.length||e.sel!==r.sel)return!1;for(let t=0;t{var t;if((t=R.info)==null||t.call(R,\`setRowCount \${e}\`),e{let s=this.bufferSize*.25;return m(this,h).to-t0&&e-m(this,h).from0&&this.clientRange.from+this.rowsWithinRange===this.rowCount}outOfRange(e,t){let{from:s,to:n}=this.range;if(t=n)return!0}setAtIndex(e){let{rowIndex:t}=e,s=t-m(this,h).from;if(lt(e,this.internalData[s]))return!1;let n=this.isWithinClientRange(t);return(n||this.isWithinRange(t))&&(!this.internalData[s]&&n&&(this.rowsWithinRange+=1),this.internalData[s]=e),n}getAtIndex(e){return m(this,h).isWithin(e)&&this.internalData[e-m(this,h).from]!=null?this.internalData[e-m(this,h).from]:void 0}isWithinRange(e){return m(this,h).isWithin(e)}isWithinClientRange(e){return this.clientRange.isWithin(e)}setClientRange(e,t){var p;(p=R.debug)==null||p.call(R,\`setClientRange \${e} - \${t}\`);let s=this.clientRange.from,n=Math.min(this.clientRange.to,this.rowCount);if(e===s&&t===n)return[!1,Ue];let o=this.clientRange.copy();this.clientRange.from=e,this.clientRange.to=t,this.rowsWithinRange=0;for(let a=e;ao.to){let a=Math.max(e,o.to);i=this.internalData.slice(a-u,t-u)}else{let a=Math.min(o.from,t);i=this.internalData.slice(e-u,a-u)}return[this.bufferBreakout(e,t),i]}setRange(e,t){var s,n;if(e!==m(this,h).from||t!==m(this,h).to){(s=R.debug)==null||s.call(R,\`setRange \${e} - \${t}\`);let[o,i]=m(this,h).overlap(e,t),u=new Array(t-e);this.rowsWithinRange=0;for(let c=o;c=0;o--)if(e[o]!==void 0){n=e[o];break}return s&&n?[s.rowIndex,n.rowIndex]:[-1,-1]}};h=new WeakMap;var ct=[],{debug:f,debugEnabled:U,error:pt,info:d,infoEnabled:dt,warn:y}=b("viewport"),gt=({rowKey:r,updateType:e})=>e==="U"&&!r.startsWith("\$root"),W=[void 0,void 0],ht={count:0,mode:void 0,size:0,ts:0},q=class{constructor({aggregations:e,bufferSize:t=50,columns:s,filter:n,groupBy:o=[],table:i,range:u,sort:c,title:p,viewport:a,visualLink:l},g){this.batchMode=!0;this.hasUpdates=!1;this.pendingUpdates=[];this.pendingOperations=new Map;this.pendingRangeRequests=[];this.rowCountChanged=!1;this.selectedRows=[];this.useBatchMode=!0;this.lastUpdateStatus=ht;this.updateThrottleTimer=void 0;this.rangeMonitor=new k("ViewPort");this.disabled=!1;this.isTree=!1;this.status="";this.suspended=!1;this.suspendTimer=null;this.setLastSizeOnlyUpdateSize=e=>{this.lastUpdateStatus.size=e};this.setLastUpdate=e=>{let{ts:t,mode:s}=this.lastUpdateStatus,n=0;if(s===e){let o=Date.now();this.lastUpdateStatus.count+=1,this.lastUpdateStatus.ts=o,n=t===0?0:o-t}else this.lastUpdateStatus.count=1,this.lastUpdateStatus.ts=0,n=0;return this.lastUpdateStatus.mode=e,n};this.rangeRequestAlreadyPending=e=>{let{bufferSize:t}=this,s=t*.25,{from:n}=e;for(let{from:o,to:i}of this.pendingRangeRequests)if(n>=o&&n{this.updateThrottleTimer=void 0,this.lastUpdateStatus.count=3,this.postMessageToClient({clientViewportId:this.clientViewportId,mode:"size-only",size:this.lastUpdateStatus.size,type:"viewport-update"})};this.shouldThrottleMessage=e=>{let t=this.setLastUpdate(e);return e==="size-only"&&t>0&&t<500&&this.lastUpdateStatus.count>3};this.throttleMessage=e=>this.shouldThrottleMessage(e)?(d==null||d("throttling updates setTimeout to 2000"),this.updateThrottleTimer===void 0&&(this.updateThrottleTimer=setTimeout(this.sendThrottledSizeMessage,2e3)),!0):(this.updateThrottleTimer!==void 0&&(clearTimeout(this.updateThrottleTimer),this.updateThrottleTimer=void 0),!1);this.getNewRowCount=()=>{if(this.rowCountChanged&&this.dataWindow)return this.rowCountChanged=!1,this.dataWindow.rowCount};this.aggregations=e,this.bufferSize=t,this.clientRange=u,this.clientViewportId=a,this.columns=s,this.filter=n,this.groupBy=o,this.keys=new O(u),this.pendingLinkedParent=l,this.table=i,this.sort=c,this.title=p,dt&&(d==null||d(\`constructor #\${a} \${i.table} bufferSize=\${t}\`)),this.dataWindow=new N(this.clientRange,u,this.bufferSize),this.postMessageToClient=g}get hasUpdatesToProcess(){return this.suspended?!1:this.rowCountChanged||this.hasUpdates}get size(){var e;return(e=this.dataWindow.rowCount)!=null?e:0}subscribe(){let{filter:e}=this.filter;return this.status=this.status==="subscribed"?"resubscribing":"subscribing",{type:we,table:this.table,range:G(this.clientRange,this.bufferSize),aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:e}}}handleSubscribed({viewPortId:e,aggregations:t,columns:s,filterSpec:n,range:o,sort:i,groupBy:u},c){return this.serverViewportId=e,this.status="subscribed",this.aggregations=t,this.columns=s,this.groupBy=u,this.isTree=u&&u.length>0,this.dataWindow.setRange(o.from,o.to),{aggregations:t,type:"subscribed",clientViewportId:this.clientViewportId,columns:s,filter:n,groupBy:u,range:o,sort:i,tableSchema:c}}awaitOperation(e,t){this.pendingOperations.set(e,t)}completeOperation(e,...t){var u;let{clientViewportId:s,pendingOperations:n}=this,o=n.get(e);if(!o){pt(\`no matching operation found to complete for requestId \${e}\`);return}let{type:i}=o;if(d==null||d(\`completeOperation \${i}\`),n.delete(e),i==="CHANGE_VP_RANGE"){let[c,p]=t;(u=this.dataWindow)==null||u.setRange(c,p);for(let a=this.pendingRangeRequests.length-1;a>=0;a--){let l=this.pendingRangeRequests[a];if(l.requestId===e){l.acked=!0;break}else y==null||y("range requests sent faster than they are being ACKed")}}else if(i==="config"){let{aggregations:c,columns:p,filter:a,groupBy:l,sort:g}=o.data;return this.aggregations=c,this.columns=p,this.filter=a,this.groupBy=l,this.sort=g,l.length>0?this.isTree=!0:this.isTree&&(this.isTree=!1),f==null||f(\`config change confirmed, isTree : \${this.isTree}\`),{clientViewportId:s,type:i,config:o.data}}else{if(i==="groupBy")return this.isTree=o.data.length>0,this.groupBy=o.data,f==null||f(\`groupBy change confirmed, isTree : \${this.isTree}\`),{clientViewportId:s,type:i,groupBy:o.data};if(i==="columns")return this.columns=o.data,{clientViewportId:s,type:i,columns:o.data};if(i==="filter")return this.filter=o.data,{clientViewportId:s,type:i,filter:o.data};if(i==="aggregate")return this.aggregations=o.data,{clientViewportId:s,type:"aggregate",aggregations:this.aggregations};if(i==="sort")return this.sort=o.data,{clientViewportId:s,type:i,sort:this.sort};if(i!=="selection"){if(i==="disable")return this.disabled=!0,{type:"disabled",clientViewportId:s};if(i==="enable")return this.disabled=!1,{type:"enabled",clientViewportId:s};if(i==="CREATE_VISUAL_LINK"){let[c,p,a]=t;return this.linkedParent={colName:c,parentViewportId:p,parentColName:a},this.pendingLinkedParent=void 0,{type:"vuu-link-created",clientViewportId:s,colName:c,parentViewportId:p,parentColName:a}}else if(i==="REMOVE_VISUAL_LINK")return this.linkedParent=void 0,{type:"vuu-link-removed",clientViewportId:s}}}}rangeRequest(e,t){U&&this.rangeMonitor.set(t);let s="CHANGE_VP_RANGE";if(this.dataWindow){let[n,o]=this.dataWindow.setClientRange(t.from,t.to),i,u=this.dataWindow.rowCount||void 0,c=n&&!this.rangeRequestAlreadyPending(t)?{type:s,viewPortId:this.serverViewportId,...G(t,this.bufferSize,u)}:null;if(c){U&&(f==null||f(\`create CHANGE_VP_RANGE: [\${c.from} - \${c.to}]\`)),this.awaitOperation(e,{type:s});let a=this.pendingRangeRequests.at(-1);if(a)if(a.acked)console.warn("Range Request before previous request is filled");else{let{from:l,to:g}=a;this.dataWindow.outOfRange(l,g)?i={clientViewportId:this.clientViewportId,type:"debounce-begin"}:y==null||y("Range Request before previous request is acked")}this.pendingRangeRequests.push({...c,requestId:e}),this.useBatchMode&&(this.batchMode=!0)}else o.length>0&&(this.batchMode=!1);this.keys.reset(this.dataWindow.clientRange);let p=this.isTree?j:J;return o.length?[c,o.map(a=>p(a,this.keys,this.selectedRows))]:i?[c,void 0,i]:[c]}else return[null]}setLinks(e){return this.links=e,[{type:"vuu-links",links:e,clientViewportId:this.clientViewportId},this.pendingLinkedParent]}setMenu(e){return{type:"vuu-menu",menu:e,clientViewportId:this.clientViewportId}}openTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:Le,vpId:this.serverViewportId,treeKey:t.key}}closeTreeNode(e,t){return this.useBatchMode&&(this.batchMode=!0),{type:be,vpId:this.serverViewportId,treeKey:t.key}}createLink(e,t,s,n){let o={type:"CREATE_VISUAL_LINK",parentVpId:s,childVpId:this.serverViewportId,parentColumnName:n,childColumnName:t};return this.awaitOperation(e,o),this.useBatchMode&&(this.batchMode=!0),o}removeLink(e){let t={type:"REMOVE_VISUAL_LINK",childVpId:this.serverViewportId};return this.awaitOperation(e,t),t}suspend(){this.suspended=!0,d==null||d("suspend")}resume(){return this.suspended=!1,U&&(f==null||f(\`resume: \${this.currentData()}\`)),[this.size,this.currentData()]}currentData(){let e=[];if(this.dataWindow){let t=this.dataWindow.getData(),{keys:s}=this,n=this.isTree?j:J;for(let o of t)o&&e.push(n(o,s,this.selectedRows))}return e}enable(e){return this.awaitOperation(e,{type:"enable"}),d==null||d(\`enable: \${this.serverViewportId}\`),{type:ye,viewPortId:this.serverViewportId}}disable(e){return this.awaitOperation(e,{type:"disable"}),d==null||d(\`disable: \${this.serverViewportId}\`),this.suspended=!1,{type:Ve,viewPortId:this.serverViewportId}}columnRequest(e,t){return this.awaitOperation(e,{type:"columns",data:t}),f==null||f(\`columnRequest: \${t}\`),this.createRequest({columns:t})}filterRequest(e,t){this.awaitOperation(e,{type:"filter",data:t}),this.useBatchMode&&(this.batchMode=!0);let{filter:s}=t;return d==null||d(\`filterRequest: \${s}\`),this.createRequest({filterSpec:{filter:s}})}setConfig(e,t){this.awaitOperation(e,{type:"config",data:t});let{filter:s,...n}=t;return this.useBatchMode&&(this.batchMode=!0),U?f==null||f(\`setConfig \${JSON.stringify(t)}\`):d==null||d("setConfig"),this.createRequest({...n,filterSpec:typeof(s==null?void 0:s.filter)=="string"?{filter:s.filter}:{filter:""}},!0)}aggregateRequest(e,t){return this.awaitOperation(e,{type:"aggregate",data:t}),d==null||d(\`aggregateRequest: \${t}\`),this.createRequest({aggregations:t})}sortRequest(e,t){return this.awaitOperation(e,{type:"sort",data:t}),d==null||d(\`sortRequest: \${JSON.stringify(t.sortDefs)}\`),this.createRequest({sort:t})}groupByRequest(e,t=ct){var s;return this.awaitOperation(e,{type:"groupBy",data:t}),this.useBatchMode&&(this.batchMode=!0),this.isTree||(s=this.dataWindow)==null||s.clear(),this.createRequest({groupBy:t})}selectRequest(e,t){return this.selectedRows=t,this.awaitOperation(e,{type:"selection",data:t}),d==null||d(\`selectRequest: \${t}\`),{type:"SET_SELECTION",vpId:this.serverViewportId,selection:he(t)}}removePendingRangeRequest(e,t){for(let s=this.pendingRangeRequests.length-1;s>=0;s--){let{from:n,to:o}=this.pendingRangeRequests[s],i=!0;if(e>=n&&en&&t0){e=[],t="update";for(let i of this.pendingUpdates)e.push(o(i,s,n));this.pendingUpdates.length=0}else{let i=this.dataWindow.getData();if(this.dataWindow.hasAllRowsWithinRange){e=[],t="batch";for(let u of i)e.push(o(u,s,n));this.batchMode=!1}}this.hasUpdates=!1}return this.throttleMessage(t)?W:[e,t]}createRequest(e,t=!1){return t?{type:"CHANGE_VP",viewPortId:this.serverViewportId,...e}:{type:"CHANGE_VP",viewPortId:this.serverViewportId,aggregations:this.aggregations,columns:this.columns,sort:this.sort,groupBy:this.groupBy,filterSpec:{filter:this.filter.filter},...e}}},J=({rowIndex:r,rowKey:e,sel:t,data:s},n,o)=>[r,n.keyFor(r),!0,!1,0,0,e,t?z(o,r):0].concat(s),j=({rowIndex:r,rowKey:e,sel:t,data:s},n,o)=>{let[i,u,,c,,p,...a]=s;return[r,n.keyFor(r),c,u,i,p,e,t?z(o,r):0].concat(a)};var We=1;var{debug:V,debugEnabled:Q,error:M,info:T,infoEnabled:ft,warn:Y}=b("server-proxy"),C=()=>\`\${We++}\`,mt={},Ct=r=>r.disabled!==!0&&r.suspended!==!0,Rt={type:"NO_ACTION"},St=(r,e,t)=>r.map(s=>s.parentVpId===e?{...s,label:t}:s);function Tt(r,e){return r.map(t=>{let{parentVpId:s}=t,n=e.get(s);if(n)return{...t,parentClientVpId:n.clientViewportId,label:n.title};throw Error("addLabelsToLinks viewport not found")})}var F=class{constructor(e,t){this.authToken="";this.user="user";this.pendingRequests=new Map;this.queuedRequests=[];this.cachedTableMetaRequests=new Map;this.cachedTableSchemas=new Map;this.connection=e,this.postMessageToClient=t,this.viewports=new Map,this.mapClientToServerViewport=new Map}async reconnect(){await this.login(this.authToken);let[e,t]=le(Array.from(this.viewports.values()),Ct);this.viewports.clear(),this.mapClientToServerViewport.clear();let s=n=>{n.forEach(o=>{let{clientViewportId:i}=o;this.viewports.set(i,o),this.sendMessageToServer(o.subscribe(),i)})};s(e),setTimeout(()=>{s(t)},2e3)}async login(e,t="user"){if(e)return this.authToken=e,this.user=t,new Promise((s,n)=>{this.sendMessageToServer({type:Pe,token:this.authToken,user:t},""),this.pendingLogin={resolve:s,reject:n}});this.authToken===""&&M("login, cannot login until auth token has been obtained")}subscribe(e){if(this.mapClientToServerViewport.has(e.viewport))M(\`spurious subscribe call \${e.viewport}\`);else{let t=this.getTableMeta(e.table),s=new q(e,this.postMessageToClient);this.viewports.set(e.viewport,s);let n=this.awaitResponseToMessage(s.subscribe(),e.viewport);Promise.all([n,t]).then(([i,u])=>{let{viewPortId:c}=i,{status:p}=s;e.viewport!==c&&(this.viewports.delete(e.viewport),this.viewports.set(c,s)),this.mapClientToServerViewport.set(e.viewport,c);let a=s.handleSubscribed(i,u);a&&(this.postMessageToClient(a),Q&&V(\`post DataSourceSubscribedMessage to client: \${JSON.stringify(a)}\`)),s.disabled&&this.disableViewport(s),this.queuedRequests.length>0&&this.processQueuedRequests(),p==="subscribing"&&!x(s.table)&&(this.sendMessageToServer({type:K,vpId:c}),this.sendMessageToServer({type:_e,vpId:c}),Array.from(this.viewports.entries()).filter(([l,{disabled:g}])=>l!==c&&!g).forEach(([l])=>{this.sendMessageToServer({type:K,vpId:l})}))})}}processQueuedRequests(){let e={};for(;this.queuedRequests.length;){let t=this.queuedRequests.pop();if(t){let{clientViewportId:s,message:n,requestId:o}=t;if(n.type==="CHANGE_VP_RANGE"){if(e.CHANGE_VP_RANGE)continue;e.CHANGE_VP_RANGE=!0;let i=this.mapClientToServerViewport.get(s);i&&this.sendMessageToServer({...n,viewPortId:i},o)}}}}unsubscribe(e){let t=this.mapClientToServerViewport.get(e);t?(T==null||T(\`Unsubscribe Message (Client to Server): - \${t}\`),this.sendMessageToServer({type:Oe,viewPortId:t})):M(\`failed to unsubscribe client viewport \${e}, viewport not found\`)}getViewportForClient(e,t=!0){let s=this.mapClientToServerViewport.get(e);if(s){let n=this.viewports.get(s);if(n)return n;if(t)throw Error(\`Viewport not found for client viewport \${e}\`);return null}else{if(this.viewports.has(e))return this.viewports.get(e);if(t)throw Error(\`Viewport server id not found for client viewport \${e}\`);return null}}setViewRange(e,t){let s=C(),[n,o,i]=e.rangeRequest(s,t.range);T==null||T(\`setViewRange \${t.range.from} - \${t.range.to}\`),n&&(this.sendIfReady(n,s,e.status==="subscribed")||this.queuedRequests.push({clientViewportId:t.viewport,message:n,requestId:s})),o?(T==null||T(\`setViewRange \${o.length} rows returned from cache\`),this.postMessageToClient({mode:"batch",type:"viewport-update",clientViewportId:e.clientViewportId,rows:o})):i&&this.postMessageToClient(i)}setConfig(e,t){let s=C(),n=e.setConfig(s,t.config);this.sendIfReady(n,s,e.status==="subscribed")}aggregate(e,t){let s=C(),n=e.aggregateRequest(s,t.aggregations);this.sendIfReady(n,s,e.status==="subscribed")}sort(e,t){let s=C(),n=e.sortRequest(s,t.sort);this.sendIfReady(n,s,e.status==="subscribed")}groupBy(e,t){let s=C(),n=e.groupByRequest(s,t.groupBy);this.sendIfReady(n,s,e.status==="subscribed")}filter(e,t){let s=C(),{filter:n}=t,o=e.filterRequest(s,n);this.sendIfReady(o,s,e.status==="subscribed")}setColumns(e,t){let s=C(),{columns:n}=t,o=e.columnRequest(s,n);this.sendIfReady(o,s,e.status==="subscribed")}setTitle(e,t){e&&(e.title=t.title,this.updateTitleOnVisualLinks(e))}select(e,t){let s=C(),{selected:n}=t,o=e.selectRequest(s,n);this.sendIfReady(o,s,e.status==="subscribed")}disableViewport(e){let t=C(),s=e.disable(t);this.sendIfReady(s,t,e.status==="subscribed")}enableViewport(e){if(e.disabled){let t=C(),s=e.enable(t);this.sendIfReady(s,t,e.status==="subscribed")}}suspendViewport(e){e.suspend(),e.suspendTimer=setTimeout(()=>{T==null||T("suspendTimer expired, escalate suspend to disable"),this.disableViewport(e)},3e3)}resumeViewport(e){e.suspendTimer&&(V==null||V("clear suspend timer"),clearTimeout(e.suspendTimer),e.suspendTimer=null);let[t,s]=e.resume();V==null||V(\`resumeViewport size \${t}, \${s.length} rows sent to client\`),this.postMessageToClient({clientViewportId:e.clientViewportId,mode:"batch",rows:s,size:t,type:"viewport-update"})}openTreeNode(e,t){if(e.serverViewportId){let s=C();this.sendIfReady(e.openTreeNode(s,t),s,e.status==="subscribed")}}closeTreeNode(e,t){if(e.serverViewportId){let s=C();this.sendIfReady(e.closeTreeNode(s,t),s,e.status==="subscribed")}}createLink(e,t){let{parentClientVpId:s,parentColumnName:n,childColumnName:o}=t,i=C(),u=this.mapClientToServerViewport.get(s);if(u){let c=e.createLink(i,o,u,n);this.sendMessageToServer(c,i)}else M("ServerProxy unable to create link, viewport not found")}removeLink(e){let t=C(),s=e.removeLink(t);this.sendMessageToServer(s,t)}updateTitleOnVisualLinks(e){var n;let{serverViewportId:t,title:s}=e;for(let o of this.viewports.values())if(o!==e&&o.links&&t&&s&&(n=o.links)!=null&&n.some(i=>i.parentVpId===t)){let[i]=o.setLinks(St(o.links,t,s));this.postMessageToClient(i)}}removeViewportFromVisualLinks(e){var t;for(let s of this.viewports.values())if((t=s.links)!=null&&t.some(({parentVpId:n})=>n===e)){let[n]=s.setLinks(s.links.filter(({parentVpId:o})=>o!==e));this.postMessageToClient(n)}}menuRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[s,n]=A(e);this.sendMessageToServer({...n,vpId:t.serverViewportId},s)}}viewportRpcCall(e){let t=this.getViewportForClient(e.vpId,!1);if(t!=null&&t.serverViewportId){let[s,n]=A(e);this.sendMessageToServer({...n,vpId:t.serverViewportId,namedParams:{}},s)}}rpcCall(e){let[t,s]=A(e),n=Ne(s.service);this.sendMessageToServer(s,t,{module:n})}handleMessageFromClient(e){var t;if(ge(e))if(e.type==="disable"){let s=this.getViewportForClient(e.viewport,!1);return s!==null?this.disableViewport(s):void 0}else{let s=this.getViewportForClient(e.viewport);switch(e.type){case"setViewRange":return this.setViewRange(s,e);case"config":return this.setConfig(s,e);case"aggregate":return this.aggregate(s,e);case"sort":return this.sort(s,e);case"groupBy":return this.groupBy(s,e);case"filter":return this.filter(s,e);case"select":return this.select(s,e);case"suspend":return this.suspendViewport(s);case"resume":return this.resumeViewport(s);case"enable":return this.enableViewport(s);case"openTreeNode":return this.openTreeNode(s,e);case"closeTreeNode":return this.closeTreeNode(s,e);case"createLink":return this.createLink(s,e);case"removeLink":return this.removeLink(s);case"setColumns":return this.setColumns(s,e);case"setTitle":return this.setTitle(s,e);default:}}else{if(Ce(e))return this.viewportRpcCall(e);if(me(e))return this.menuRpcCall(e);{let{type:s,requestId:n}=e;switch(s){case"GET_TABLE_LIST":{(t=this.tableList)!=null||(this.tableList=this.awaitResponseToMessage({type:s},n)),this.tableList.then(o=>{this.postMessageToClient({type:"TABLE_LIST_RESP",tables:o.tables,requestId:n})});return}case"GET_TABLE_META":{this.getTableMeta(e.table,n).then(o=>{o&&this.postMessageToClient({type:"TABLE_META_RESP",tableSchema:o,requestId:n})});return}case"RPC_CALL":return this.rpcCall(e);default:}}}M(\`Vuu ServerProxy Unexpected message from client \${JSON.stringify(e)}\`)}getTableMeta(e,t=C()){if(x(e))return Promise.resolve(void 0);let s=\`\${e.module}:\${e.table}\`,n=this.cachedTableMetaRequests.get(s);return n||(n=this.awaitResponseToMessage({type:"GET_TABLE_META",table:e},t),this.cachedTableMetaRequests.set(s,n)),n==null?void 0:n.then(o=>this.cacheTableMeta(o))}awaitResponseToMessage(e,t=C()){return new Promise((s,n)=>{this.sendMessageToServer(e,t),this.pendingRequests.set(t,{reject:n,resolve:s})})}sendIfReady(e,t,s=!0){return s&&this.sendMessageToServer(e,t),s}sendMessageToServer(e,t=\`\${We++}\`,s=mt){let{module:n="CORE"}=s;this.authToken&&this.connection.send({requestId:t,sessionId:this.sessionId,token:this.authToken,user:this.user,module:n,body:e})}handleMessageFromServer(e){var u;let{body:t,requestId:s,sessionId:n}=e,o=this.pendingRequests.get(s);if(o){let{resolve:a}=o;this.pendingRequests.delete(s),a(t);return}let{viewports:i}=this;switch(t.type){case Ie:this.sendMessageToServer({type:De,ts:+new Date},"NA");break;case"LOGIN_SUCCESS":if(n)this.sessionId=n,(u=this.pendingLogin)==null||u.resolve(n),this.pendingLogin=void 0;else throw Error("LOGIN_SUCCESS did not provide sessionId");break;case"REMOVE_VP_SUCCESS":{let a=i.get(t.viewPortId);a&&(this.mapClientToServerViewport.delete(a.clientViewportId),i.delete(t.viewPortId),this.removeViewportFromVisualLinks(t.viewPortId))}break;case xe:{let a=this.viewports.get(t.vpId);a&&a.completeOperation(s)}break;case Te:case ve:if(i.has(t.viewPortId)){let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(s);l!==void 0&&(this.postMessageToClient(l),Q&&V(\`postMessageToClient \${JSON.stringify(l)}\`))}}break;case Me:{let a=this.viewports.get(t.viewPortId);if(a){let l=a.completeOperation(s);if(l){this.postMessageToClient(l);let[g,S]=a.resume();this.postMessageToClient({clientViewportId:a.clientViewportId,mode:"batch",rows:S,size:g,type:"viewport-update"})}}}break;case"TABLE_ROW":{let a=Se(t.rows);for(let[l,g]of Object.entries(a)){let S=i.get(l);S?S.updateRows(g):Y==null||Y(\`TABLE_ROW message received for non registered viewport \${l}\`)}this.processUpdates()}break;case"CHANGE_VP_RANGE_SUCCESS":{let a=this.viewports.get(t.viewPortId);if(a){let{from:l,to:g}=t;a.completeOperation(s,l,g)}}break;case ke:case Ee:break;case"CREATE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId),l=this.viewports.get(t.parentVpId);if(a&&l){let{childColumnName:g,parentColumnName:S}=t,I=a.completeOperation(s,g,l.clientViewportId,S);I&&this.postMessageToClient(I)}}break;case"REMOVE_VISUAL_LINK_SUCCESS":{let a=this.viewports.get(t.childVpId);if(a){let l=a.completeOperation(s);l&&this.postMessageToClient(l)}}break;case"VP_VISUAL_LINKS_RESP":{let a=this.getActiveLinks(t.links),l=this.viewports.get(t.vpId);if(a.length&&l){let g=Tt(a,this.viewports),[S,I]=l.setLinks(g);if(this.postMessageToClient(S),I){let{link:ne,parentClientVpId:Ke}=I,re=C(),oe=this.mapClientToServerViewport.get(Ke);if(oe){let Je=l.createLink(re,ne.fromColumn,oe,ne.toColumn);this.sendMessageToServer(Je,re)}}}}break;case"VIEW_PORT_MENUS_RESP":if(t.menu.name){let a=this.viewports.get(t.vpId);if(a){let l=a.setMenu(t.menu);this.postMessageToClient(l)}}break;case"VP_EDIT_RPC_RESPONSE":this.postMessageToClient({action:t.action,requestId:s,rpcName:t.rpcName,type:"VP_EDIT_RPC_RESPONSE"});break;case"VP_EDIT_RPC_REJECT":this.viewports.get(t.vpId)&&this.postMessageToClient({requestId:s,type:"VP_EDIT_RPC_REJECT",error:t.error});break;case"VIEW_PORT_MENU_REJ":{console.log("send menu error back to client");let{error:a,rpcName:l,vpId:g}=t,S=this.viewports.get(g);S&&this.postMessageToClient({clientViewportId:S.clientViewportId,error:a,rpcName:l,type:"VIEW_PORT_MENU_REJ",requestId:s});break}case"VIEW_PORT_MENU_RESP":if(fe(t)){let{action:a,rpcName:l}=t;this.awaitResponseToMessage({type:"GET_TABLE_META",table:a.table}).then(g=>{let S=H(g);this.postMessageToClient({rpcName:l,type:"VIEW_PORT_MENU_RESP",action:{...a,tableSchema:S},tableAlreadyOpen:this.isTableOpen(a.table),requestId:s})})}else{let{action:a}=t;this.postMessageToClient({type:"VIEW_PORT_MENU_RESP",action:a||Rt,tableAlreadyOpen:a!==null&&this.isTableOpen(a.table),requestId:s})}break;case"RPC_RESP":{let{method:a,result:l}=t;this.postMessageToClient({type:"RPC_RESP",method:a,result:l,requestId:s})}break;case"VIEW_PORT_RPC_REPONSE":{let{method:a,action:l}=t;this.postMessageToClient({type:"VIEW_PORT_RPC_RESPONSE",rpcName:a,action:l,requestId:s})}break;case"ERROR":M(t.msg);break;default:ft&&T(\`handleMessageFromServer \${t.type}.\`)}}cacheTableMeta(e){let{module:t,table:s}=e.table,n=\`\${t}:\${s}\`,o=this.cachedTableSchemas.get(n);return o||(o=H(e),this.cachedTableSchemas.set(n,o)),o}isTableOpen(e){if(e){let t=e.table;for(let s of this.viewports.values())if(!s.suspended&&s.table.table===t)return!0}}getActiveLinks(e){return e.filter(t=>{let s=this.viewports.get(t.parentVpId);return s&&!s.suspended})}processUpdates(){this.viewports.forEach(e=>{var t;if(e.hasUpdatesToProcess){let s=e.getClientRows();if(s!==W){let[n,o]=s,i=e.getNewRowCount();(i!==void 0||n&&n.length>0)&&(Q&&V(\`postMessageToClient #\${e.clientViewportId} viewport-update \${o}, \${(t=n==null?void 0:n.length)!=null?t:"no"} rows, size \${i}\`),o&&this.postMessageToClient({clientViewportId:e.clientViewportId,mode:o,rows:n,size:i,type:"viewport-update"}))}}})}};var{debug:us,debugEnabled:ls,error:qe,info:E,infoEnabled:bt,warn:_}=b("websocket-connection"),Fe="ws",Et=r=>r.startsWith(Fe+"://")||r.startsWith(Fe+"s://"),Ge={},X=Symbol("setWebsocket"),B=Symbol("connectionCallback");async function ze(r,e,t,s=10,n=5){return Ge[r]={status:"connecting",connect:{allowed:n,remaining:n},reconnect:{allowed:s,remaining:s}},He(r,e,t)}async function Z(r){throw Error("connection broken")}async function He(r,e,t,s){let{status:n,connect:o,reconnect:i}=Ge[r],u=n==="connecting"?o:i;try{t({type:"connection-status",status:"connecting"});let c=typeof s<"u",p=await Vt(r,e);console.info("%c\u26A1 %cconnected","font-size: 24px;color: green;font-weight: bold;","color:green; font-size: 14px;"),s!==void 0&&s[X](p);let a=s!=null?s:new ee(p,r,e,t),l=c?"reconnected":"connection-open-awaiting-session";return t({type:"connection-status",status:l}),a.status=l,u.remaining=u.allowed,a}catch{let p=--u.remaining>0;if(t({type:"connection-status",status:"disconnected",reason:"failed to connect",retry:p}),p)return wt(r,e,t,s,2e3);throw Error("Failed to establish connection")}}var wt=(r,e,t,s,n)=>new Promise(o=>{setTimeout(()=>{o(He(r,e,t,s))},n)}),Vt=(r,e)=>new Promise((t,s)=>{let n=Et(r)?r:\`wss://\${r}\`;bt&&e!==void 0&&E(\`WebSocket Protocol \${e==null?void 0:e.toString()}\`);let o=new WebSocket(n,e);o.onopen=()=>t(o),o.onerror=i=>s(i)}),Be=()=>{_==null||_("Connection cannot be closed, socket not yet opened")},\$e=r=>{_==null||_(\`Message cannot be sent, socket closed \${r.body.type}\`)},vt=r=>{try{return JSON.parse(r)}catch{throw Error(\`Error parsing JSON response from server \${r}\`)}},ee=class{constructor(e,t,s,n){this.close=Be;this.requiresLogin=!0;this.send=\$e;this.status="ready";this.messagesCount=0;this.connectionMetricsInterval=null;this.handleWebsocketMessage=e=>{let t=vt(e.data);this.messagesCount+=1,this[B](t)};this.url=t,this.protocol=s,this[B]=n,this[X](e)}reconnect(){Z(this)}[(B,X)](e){let t=this[B];e.onmessage=o=>{this.status="connected",e.onmessage=this.handleWebsocketMessage,this.handleWebsocketMessage(o)},this.connectionMetricsInterval=setInterval(()=>{t({type:"connection-metrics",messagesLength:this.messagesCount}),this.messagesCount=0},2e3),e.onerror=()=>{qe("\u26A1 connection error"),t({type:"connection-status",status:"disconnected",reason:"error"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status==="connection-open-awaiting-session"?qe("Websocket connection lost before Vuu session established, check websocket configuration"):this.status!=="closed"&&(Z(this),this.send=n)},e.onclose=()=>{E==null||E("\u26A1 connection close"),t({type:"connection-status",status:"disconnected",reason:"close"}),this.connectionMetricsInterval&&(clearInterval(this.connectionMetricsInterval),this.connectionMetricsInterval=null),this.status!=="closed"&&(Z(this),this.send=n)};let s=o=>{e.send(JSON.stringify(o))},n=o=>{E==null||E(\`TODO queue message until websocket reconnected \${o.body.type}\`)};this.send=s,this.close=()=>{this.status="closed",e.close(),this.close=Be,this.send=\$e,E==null||E("close websocket")}}};var v,{info:te,infoEnabled:se}=b("worker");async function yt(r,e,t,s,n,o,i){let u=await ze(r,e,c=>{de(c)?postMessage({type:"connection-metrics",messages:c}):pe(c)?(n(c),c.status==="reconnected"&&v.reconnect()):v.handleMessageFromServer(c)},o,i);v=new F(u,c=>Mt(c)),u.requiresLogin&&await v.login(t,s)}function Mt(r){postMessage(r)}var _t=async({data:r})=>{switch(r.type){case"connect":await yt(r.url,r.protocol,r.token,r.username,postMessage,r.retryLimitDisconnect,r.retryLimitStartup),postMessage({type:"connected"});break;case"subscribe":se&&te(\`client subscribe: \${JSON.stringify(r)}\`),v.subscribe(r);break;case"unsubscribe":se&&te(\`client unsubscribe: \${JSON.stringify(r)}\`),v.unsubscribe(r.viewport);break;default:se&&te(\`client message: \${JSON.stringify(r)}\`),v.handleMessageFromClient(r)}};self.addEventListener("message",_t);postMessage({type:"ready"}); + \`); + throw Error(\`KeySet, no key found for rowIndex \${rowIndex}\`); + } + return key; + } + toDebugString() { + return Array.from(this.keys.entries()).map(([k, v]) => \`\${k}=>\${v}\`).join(","); + } +}; + +// ../vuu-utils/src/selection-utils.ts +var { SELECTED } = metadataKeys; +var RowSelected = { + False: 0, + True: 1, + First: 2, + Last: 4 +}; +var rangeIncludes = (range, index) => index >= range[0] && index <= range[1]; +var SINGLE_SELECTED_ROW = RowSelected.True + RowSelected.First + RowSelected.Last; +var FIRST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.First; +var LAST_SELECTED_ROW_OF_BLOCK = RowSelected.True + RowSelected.Last; +var getSelectionStatus = (selected, itemIndex) => { + for (const item of selected) { + if (typeof item === "number") { + if (item === itemIndex) { + return SINGLE_SELECTED_ROW; + } + } else if (rangeIncludes(item, itemIndex)) { + if (itemIndex === item[0]) { + return FIRST_SELECTED_ROW_OF_BLOCK; + } else if (itemIndex === item[1]) { + return LAST_SELECTED_ROW_OF_BLOCK; + } else { + return RowSelected.True; + } + } + } + return RowSelected.False; +}; +var expandSelection = (selected) => { + if (selected.every((selectedItem) => typeof selectedItem === "number")) { + return selected; + } + const expandedSelected = []; + for (const selectedItem of selected) { + if (typeof selectedItem === "number") { + expandedSelected.push(selectedItem); + } else { + for (let i = selectedItem[0]; i <= selectedItem[1]; i++) { + expandedSelected.push(i); + } + } + } + return expandedSelected; +}; + +// src/data-source.ts +var isSessionTableActionMessage = (messageBody) => messageBody.type === "VIEW_PORT_MENU_RESP" && messageBody.action !== null && isSessionTable(messageBody.action.table); +var isSessionTable = (table) => { + if (table !== null && typeof table === "object" && "table" in table && "module" in table) { + return table.table.startsWith("session"); + } + return false; +}; + +// src/message-utils.ts +var MENU_RPC_TYPES = [ + "VIEW_PORT_MENUS_SELECT_RPC", + "VIEW_PORT_MENU_TABLE_RPC", + "VIEW_PORT_MENU_ROW_RPC", + "VIEW_PORT_MENU_CELL_RPC", + "VP_EDIT_CELL_RPC", + "VP_EDIT_ROW_RPC", + "VP_EDIT_ADD_ROW_RPC", + "VP_EDIT_DELETE_CELL_RPC", + "VP_EDIT_DELETE_ROW_RPC", + "VP_EDIT_SUBMIT_FORM_RPC" +]; +var isVuuMenuRpcRequest = (message) => MENU_RPC_TYPES.includes(message["type"]); +var isVuuRpcRequest = (message) => message["type"] === "VIEW_PORT_RPC_CALL"; +var stripRequestId = ({ + requestId, + ...rest +}) => [requestId, rest]; +var getFirstAndLastRows = (rows) => { + let firstRow = rows.at(0); + if (firstRow.updateType === "SIZE") { + if (rows.length === 1) { + return rows; + } else { + firstRow = rows.at(1); + } + } + const lastRow = rows.at(-1); + return [firstRow, lastRow]; +}; +var groupRowsByViewport = (rows) => { + const result = {}; + for (const row of rows) { + const rowsForViewport = result[row.viewPortId] || (result[row.viewPortId] = []); + rowsForViewport.push(row); + } + return result; +}; +var createSchemaFromTableMetadata = ({ + columns, + dataTypes, + key, + table +}) => { + return { + table, + columns: columns.map((col, idx) => ({ + name: col, + serverDataType: dataTypes[idx] + })), + key + }; +}; + +// src/server-proxy/messages.ts +var CHANGE_VP_SUCCESS = "CHANGE_VP_SUCCESS"; +var CLOSE_TREE_NODE = "CLOSE_TREE_NODE"; +var CLOSE_TREE_SUCCESS = "CLOSE_TREE_SUCCESS"; +var CREATE_VP = "CREATE_VP"; +var DISABLE_VP = "DISABLE_VP"; +var DISABLE_VP_SUCCESS = "DISABLE_VP_SUCCESS"; +var ENABLE_VP = "ENABLE_VP"; +var ENABLE_VP_SUCCESS = "ENABLE_VP_SUCCESS"; +var GET_VP_VISUAL_LINKS = "GET_VP_VISUAL_LINKS"; +var GET_VIEW_PORT_MENUS = "GET_VIEW_PORT_MENUS"; +var HB = "HB"; +var HB_RESP = "HB_RESP"; +var LOGIN = "LOGIN"; +var OPEN_TREE_NODE = "OPEN_TREE_NODE"; +var OPEN_TREE_SUCCESS = "OPEN_TREE_SUCCESS"; +var REMOVE_VP = "REMOVE_VP"; +var SET_SELECTION_SUCCESS = "SET_SELECTION_SUCCESS"; + +// src/server-proxy/rpc-services.ts +var getRpcServiceModule = (service) => { + switch (service) { + case "TypeAheadRpcHandler": + return "TYPEAHEAD"; + default: + return "SIMUL"; + } +}; + +// src/server-proxy/array-backed-moving-window.ts +var EMPTY_ARRAY = []; +var log = logger("array-backed-moving-window"); +function dataIsUnchanged(newRow, existingRow) { + if (!existingRow) { + return false; + } + if (existingRow.data.length !== newRow.data.length) { + return false; + } + if (existingRow.sel !== newRow.sel) { + return false; + } + for (let i = 0; i < existingRow.data.length; i++) { + if (existingRow.data[i] !== newRow.data[i]) { + return false; + } + } + return true; +} +var _range; +var ArrayBackedMovingWindow = class { + // Note, the buffer is already accounted for in the range passed in here + constructor({ from: clientFrom, to: clientTo }, { from, to }, bufferSize) { + __privateAdd(this, _range, void 0); + this.setRowCount = (rowCount) => { + var _a; + (_a = log.info) == null ? void 0 : _a.call(log, \`setRowCount \${rowCount}\`); + if (rowCount < this.internalData.length) { + this.internalData.length = rowCount; + } + if (rowCount < this.rowCount) { + this.rowsWithinRange = 0; + const end = Math.min(rowCount, this.clientRange.to); + for (let i = this.clientRange.from; i < end; i++) { + const rowIndex = i - __privateGet(this, _range).from; + if (this.internalData[rowIndex] !== void 0) { + this.rowsWithinRange += 1; + } + } + } + this.rowCount = rowCount; + }; + this.bufferBreakout = (from, to) => { + const bufferPerimeter = this.bufferSize * 0.25; + if (__privateGet(this, _range).to - to < bufferPerimeter) { + return true; + } else if (__privateGet(this, _range).from > 0 && from - __privateGet(this, _range).from < bufferPerimeter) { + return true; + } else { + return false; + } + }; + this.bufferSize = bufferSize; + this.clientRange = new WindowRange(clientFrom, clientTo); + __privateSet(this, _range, new WindowRange(from, to)); + this.internalData = new Array(bufferSize); + this.rowsWithinRange = 0; + this.rowCount = 0; + } + get range() { + return __privateGet(this, _range); + } + // TODO we shpuld probably have a hasAllClientRowsWithinRange + get hasAllRowsWithinRange() { + return this.rowsWithinRange === this.clientRange.to - this.clientRange.from || // this.rowsWithinRange === this.range.to - this.range.from || + this.rowCount > 0 && this.clientRange.from + this.rowsWithinRange === this.rowCount; + } + // Check to see if set of rows is outside the current viewport range, indicating + // that veiwport is being scrolled quickly and server is not able to keep up. + outOfRange(firstIndex, lastIndex) { + const { from, to } = this.range; + if (lastIndex < from) { + return true; + } + if (firstIndex >= to) { + return true; + } + } + setAtIndex(row) { + const { rowIndex: index } = row; + const internalIndex = index - __privateGet(this, _range).from; + if (dataIsUnchanged(row, this.internalData[internalIndex])) { + return false; + } + const isWithinClientRange = this.isWithinClientRange(index); + if (isWithinClientRange || this.isWithinRange(index)) { + if (!this.internalData[internalIndex] && isWithinClientRange) { + this.rowsWithinRange += 1; + } + this.internalData[internalIndex] = row; + } + return isWithinClientRange; + } + getAtIndex(index) { + return __privateGet(this, _range).isWithin(index) && this.internalData[index - __privateGet(this, _range).from] != null ? this.internalData[index - __privateGet(this, _range).from] : void 0; + } + isWithinRange(index) { + return __privateGet(this, _range).isWithin(index); + } + isWithinClientRange(index) { + return this.clientRange.isWithin(index); + } + // Returns [false] or [serverDataRequired, clientRows, holdingRows] + setClientRange(from, to) { + var _a; + (_a = log.debug) == null ? void 0 : _a.call(log, \`setClientRange \${from} - \${to}\`); + const currentFrom = this.clientRange.from; + const currentTo = Math.min(this.clientRange.to, this.rowCount); + if (from === currentFrom && to === currentTo) { + return [ + false, + EMPTY_ARRAY + /*, EMPTY_ARRAY*/ + ]; + } + const originalRange = this.clientRange.copy(); + this.clientRange.from = from; + this.clientRange.to = to; + this.rowsWithinRange = 0; + for (let i = from; i < to; i++) { + const internalIndex = i - __privateGet(this, _range).from; + if (this.internalData[internalIndex]) { + this.rowsWithinRange += 1; + } + } + let clientRows = EMPTY_ARRAY; + const offset = __privateGet(this, _range).from; + if (this.hasAllRowsWithinRange) { + if (to > originalRange.to) { + const start = Math.max(from, originalRange.to); + clientRows = this.internalData.slice(start - offset, to - offset); + } else { + const end = Math.min(originalRange.from, to); + clientRows = this.internalData.slice(from - offset, end - offset); + } + } + const serverDataRequired = this.bufferBreakout(from, to); + return [serverDataRequired, clientRows]; + } + setRange(from, to) { + var _a, _b; + if (from !== __privateGet(this, _range).from || to !== __privateGet(this, _range).to) { + (_a = log.debug) == null ? void 0 : _a.call(log, \`setRange \${from} - \${to}\`); + const [overlapFrom, overlapTo] = __privateGet(this, _range).overlap(from, to); + const newData = new Array(to - from); + this.rowsWithinRange = 0; + for (let i = overlapFrom; i < overlapTo; i++) { + const data = this.getAtIndex(i); + if (data) { + const index = i - from; + newData[index] = data; + if (this.isWithinClientRange(i)) { + this.rowsWithinRange += 1; + } + } + } + this.internalData = newData; + __privateGet(this, _range).from = from; + __privateGet(this, _range).to = to; + } else { + (_b = log.debug) == null ? void 0 : _b.call(log, \`setRange \${from} - \${to} IGNORED because not changed\`); + } + } + //TODO temp + get data() { + return this.internalData; + } + getData() { + var _a; + const { from, to } = __privateGet(this, _range); + const { from: clientFrom, to: clientTo } = this.clientRange; + const startOffset = Math.max(0, clientFrom - from); + const endOffset = Math.min( + to - from, + to, + clientTo - from, + (_a = this.rowCount) != null ? _a : to + ); + return this.internalData.slice(startOffset, endOffset); + } + clear() { + var _a; + (_a = log.debug) == null ? void 0 : _a.call(log, "clear"); + this.internalData.length = 0; + this.rowsWithinRange = 0; + this.setRowCount(0); + } + // used only for debugging + getCurrentDataRange() { + const rows = this.internalData; + const len = rows.length; + let [firstRow] = this.internalData; + let lastRow = this.internalData[len - 1]; + if (firstRow && lastRow) { + return [firstRow.rowIndex, lastRow.rowIndex]; + } else { + for (let i = 0; i < len; i++) { + if (rows[i] !== void 0) { + firstRow = rows[i]; + break; + } + } + for (let i = len - 1; i >= 0; i--) { + if (rows[i] !== void 0) { + lastRow = rows[i]; + break; + } + } + if (firstRow && lastRow) { + return [firstRow.rowIndex, lastRow.rowIndex]; + } else { + return [-1, -1]; + } + } + } +}; +_range = new WeakMap(); + +// src/server-proxy/viewport.ts +var EMPTY_GROUPBY = []; +var { debug: debug2, debugEnabled: debugEnabled2, error, info, infoEnabled, warn } = logger("viewport"); +var isLeafUpdate = ({ rowKey, updateType }) => updateType === "U" && !rowKey.startsWith("\$root"); +var NO_DATA_UPDATE = [ + void 0, + void 0 +]; +var NO_UPDATE_STATUS = { + count: 0, + mode: void 0, + size: 0, + ts: 0 +}; +var Viewport = class { + constructor({ + aggregations, + bufferSize = 50, + columns, + filter, + groupBy = [], + table, + range, + sort, + title, + viewport, + visualLink + }, postMessageToClient) { + /** batchMode is irrelevant for Vuu Table, it was introduced to try and improve rendering performance of AgGrid */ + this.batchMode = true; + this.hasUpdates = false; + this.pendingUpdates = []; + this.pendingOperations = /* @__PURE__ */ new Map(); + this.pendingRangeRequests = []; + this.rowCountChanged = false; + this.selectedRows = []; + this.useBatchMode = true; + this.lastUpdateStatus = NO_UPDATE_STATUS; + this.updateThrottleTimer = void 0; + this.rangeMonitor = new RangeMonitor("ViewPort"); + this.disabled = false; + this.isTree = false; + // TODO roll disabled/suspended into status + this.status = ""; + this.suspended = false; + this.suspendTimer = null; + // Records SIZE only updates + this.setLastSizeOnlyUpdateSize = (size) => { + this.lastUpdateStatus.size = size; + }; + this.setLastUpdate = (mode) => { + const { ts: lastTS, mode: lastMode } = this.lastUpdateStatus; + let elapsedTime = 0; + if (lastMode === mode) { + const ts = Date.now(); + this.lastUpdateStatus.count += 1; + this.lastUpdateStatus.ts = ts; + elapsedTime = lastTS === 0 ? 0 : ts - lastTS; + } else { + this.lastUpdateStatus.count = 1; + this.lastUpdateStatus.ts = 0; + elapsedTime = 0; + } + this.lastUpdateStatus.mode = mode; + return elapsedTime; + }; + this.rangeRequestAlreadyPending = (range) => { + const { bufferSize } = this; + const bufferThreshold = bufferSize * 0.25; + let { from: stillPendingFrom } = range; + for (const { from, to } of this.pendingRangeRequests) { + if (stillPendingFrom >= from && stillPendingFrom < to) { + if (range.to + bufferThreshold <= to) { + return true; + } else { + stillPendingFrom = to; + } + } + } + return false; + }; + this.sendThrottledSizeMessage = () => { + this.updateThrottleTimer = void 0; + this.lastUpdateStatus.count = 3; + this.postMessageToClient({ + clientViewportId: this.clientViewportId, + mode: "size-only", + size: this.lastUpdateStatus.size, + type: "viewport-update" + }); + }; + // If we are receiving multiple SIZE updates but no data, table is loading rows + // outside of our viewport. We can safely throttle these requests. Doing so will + // alleviate pressure on UI DataTable. + this.shouldThrottleMessage = (mode) => { + const elapsedTime = this.setLastUpdate(mode); + return mode === "size-only" && elapsedTime > 0 && elapsedTime < 500 && this.lastUpdateStatus.count > 3; + }; + this.throttleMessage = (mode) => { + if (this.shouldThrottleMessage(mode)) { + info == null ? void 0 : info("throttling updates setTimeout to 2000"); + if (this.updateThrottleTimer === void 0) { + this.updateThrottleTimer = setTimeout( + this.sendThrottledSizeMessage, + 2e3 + ); + } + return true; + } else if (this.updateThrottleTimer !== void 0) { + clearTimeout(this.updateThrottleTimer); + this.updateThrottleTimer = void 0; + } + return false; + }; + this.getNewRowCount = () => { + if (this.rowCountChanged && this.dataWindow) { + this.rowCountChanged = false; + return this.dataWindow.rowCount; + } + }; + this.aggregations = aggregations; + this.bufferSize = bufferSize; + this.clientRange = range; + this.clientViewportId = viewport; + this.columns = columns; + this.filter = filter; + this.groupBy = groupBy; + this.keys = new KeySet(range); + this.pendingLinkedParent = visualLink; + this.table = table; + this.sort = sort; + this.title = title; + infoEnabled && (info == null ? void 0 : info( + \`constructor #\${viewport} \${table.table} bufferSize=\${bufferSize}\` + )); + this.dataWindow = new ArrayBackedMovingWindow( + this.clientRange, + range, + this.bufferSize + ); + this.postMessageToClient = postMessageToClient; + } + get hasUpdatesToProcess() { + if (this.suspended) { + return false; + } + return this.rowCountChanged || this.hasUpdates; + } + get size() { + var _a; + return (_a = this.dataWindow.rowCount) != null ? _a : 0; + } + subscribe() { + const { filter } = this.filter; + this.status = this.status === "subscribed" ? "resubscribing" : "subscribing"; + return { + type: CREATE_VP, + table: this.table, + range: getFullRange(this.clientRange, this.bufferSize), + aggregations: this.aggregations, + columns: this.columns, + sort: this.sort, + groupBy: this.groupBy, + filterSpec: { filter } + }; + } + handleSubscribed({ + viewPortId, + aggregations, + columns, + filterSpec: filter, + range, + sort, + groupBy + }, tableSchema) { + this.serverViewportId = viewPortId; + this.status = "subscribed"; + this.aggregations = aggregations; + this.columns = columns; + this.groupBy = groupBy; + this.isTree = groupBy && groupBy.length > 0; + this.dataWindow.setRange(range.from, range.to); + return { + aggregations, + type: "subscribed", + clientViewportId: this.clientViewportId, + columns, + filter, + groupBy, + range, + sort, + tableSchema + }; + } + awaitOperation(requestId, msg) { + this.pendingOperations.set(requestId, msg); + } + // Return a message if we need to communicate this to client UI + completeOperation(requestId, ...params) { + var _a; + const { clientViewportId, pendingOperations } = this; + const pendingOperation = pendingOperations.get(requestId); + if (!pendingOperation) { + error( + \`no matching operation found to complete for requestId \${requestId}\` + ); + return; + } + const { type } = pendingOperation; + info == null ? void 0 : info(\`completeOperation \${type}\`); + pendingOperations.delete(requestId); + if (type === "CHANGE_VP_RANGE") { + const [from, to] = params; + (_a = this.dataWindow) == null ? void 0 : _a.setRange(from, to); + for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { + const pendingRangeRequest = this.pendingRangeRequests[i]; + if (pendingRangeRequest.requestId === requestId) { + pendingRangeRequest.acked = true; + break; + } else { + warn == null ? void 0 : warn("range requests sent faster than they are being ACKed"); + } + } + } else if (type === "config") { + const { aggregations, columns, filter, groupBy, sort } = pendingOperation.data; + this.aggregations = aggregations; + this.columns = columns; + this.filter = filter; + this.groupBy = groupBy; + this.sort = sort; + if (groupBy.length > 0) { + this.isTree = true; + } else if (this.isTree) { + this.isTree = false; + } + debug2 == null ? void 0 : debug2(\`config change confirmed, isTree : \${this.isTree}\`); + return { + clientViewportId, + type, + config: pendingOperation.data + }; + } else if (type === "groupBy") { + this.isTree = pendingOperation.data.length > 0; + this.groupBy = pendingOperation.data; + debug2 == null ? void 0 : debug2(\`groupBy change confirmed, isTree : \${this.isTree}\`); + return { + clientViewportId, + type, + groupBy: pendingOperation.data + }; + } else if (type === "columns") { + this.columns = pendingOperation.data; + return { + clientViewportId, + type, + columns: pendingOperation.data + }; + } else if (type === "filter") { + this.filter = pendingOperation.data; + return { + clientViewportId, + type, + filter: pendingOperation.data + }; + } else if (type === "aggregate") { + this.aggregations = pendingOperation.data; + return { + clientViewportId, + type: "aggregate", + aggregations: this.aggregations + }; + } else if (type === "sort") { + this.sort = pendingOperation.data; + return { + clientViewportId, + type, + sort: this.sort + }; + } else if (type === "selection") { + } else if (type === "disable") { + this.disabled = true; + return { + type: "disabled", + clientViewportId + }; + } else if (type === "enable") { + this.disabled = false; + return { + type: "enabled", + clientViewportId + }; + } else if (type === "CREATE_VISUAL_LINK") { + const [colName, parentViewportId, parentColName] = params; + this.linkedParent = { + colName, + parentViewportId, + parentColName + }; + this.pendingLinkedParent = void 0; + return { + type: "vuu-link-created", + clientViewportId, + colName, + parentViewportId, + parentColName + }; + } else if (type === "REMOVE_VISUAL_LINK") { + this.linkedParent = void 0; + return { + type: "vuu-link-removed", + clientViewportId + }; + } + } + // TODO when a range request arrives, consider the viewport to be scrolling + // until data arrives and we have the full range. + // When not scrolling, any server data is an update + // When scrolling, we are in batch mode + rangeRequest(requestId, range) { + if (debugEnabled2) { + this.rangeMonitor.set(range); + } + const type = "CHANGE_VP_RANGE"; + if (this.dataWindow) { + const [serverDataRequired, clientRows] = this.dataWindow.setClientRange( + range.from, + range.to + ); + let debounceRequest; + const maxRange = this.dataWindow.rowCount || void 0; + const serverRequest = serverDataRequired && !this.rangeRequestAlreadyPending(range) ? { + type, + viewPortId: this.serverViewportId, + ...getFullRange(range, this.bufferSize, maxRange) + } : null; + if (serverRequest) { + debugEnabled2 && (debug2 == null ? void 0 : debug2( + \`create CHANGE_VP_RANGE: [\${serverRequest.from} - \${serverRequest.to}]\` + )); + this.awaitOperation(requestId, { type }); + const pendingRequest = this.pendingRangeRequests.at(-1); + if (pendingRequest) { + if (pendingRequest.acked) { + console.warn("Range Request before previous request is filled"); + } else { + const { from, to } = pendingRequest; + if (this.dataWindow.outOfRange(from, to)) { + debounceRequest = { + clientViewportId: this.clientViewportId, + type: "debounce-begin" + }; + } else { + warn == null ? void 0 : warn("Range Request before previous request is acked"); + } + } + } + this.pendingRangeRequests.push({ ...serverRequest, requestId }); + if (this.useBatchMode) { + this.batchMode = true; + } + } else if (clientRows.length > 0) { + this.batchMode = false; + } + this.keys.reset(this.dataWindow.clientRange); + const toClient = this.isTree ? toClientRowTree : toClientRow; + if (clientRows.length) { + return [ + serverRequest, + clientRows.map((row) => { + return toClient(row, this.keys, this.selectedRows); + }) + ]; + } else if (debounceRequest) { + return [serverRequest, void 0, debounceRequest]; + } else { + return [serverRequest]; + } + } else { + return [null]; + } + } + setLinks(links) { + this.links = links; + return [ + { + type: "vuu-links", + links, + clientViewportId: this.clientViewportId + }, + this.pendingLinkedParent + ]; + } + setMenu(menu) { + return { + type: "vuu-menu", + menu, + clientViewportId: this.clientViewportId + }; + } + openTreeNode(requestId, message) { + if (this.useBatchMode) { + this.batchMode = true; + } + return { + type: OPEN_TREE_NODE, + vpId: this.serverViewportId, + treeKey: message.key + }; + } + closeTreeNode(requestId, message) { + if (this.useBatchMode) { + this.batchMode = true; + } + return { + type: CLOSE_TREE_NODE, + vpId: this.serverViewportId, + treeKey: message.key + }; + } + createLink(requestId, colName, parentVpId, parentColumnName) { + const message = { + type: "CREATE_VISUAL_LINK", + parentVpId, + childVpId: this.serverViewportId, + parentColumnName, + childColumnName: colName + }; + this.awaitOperation(requestId, message); + if (this.useBatchMode) { + this.batchMode = true; + } + return message; + } + removeLink(requestId) { + const message = { + type: "REMOVE_VISUAL_LINK", + childVpId: this.serverViewportId + }; + this.awaitOperation(requestId, message); + return message; + } + suspend() { + this.suspended = true; + info == null ? void 0 : info("suspend"); + } + resume() { + this.suspended = false; + if (debugEnabled2) { + debug2 == null ? void 0 : debug2(\`resume: \${this.currentData()}\`); + } + return [this.size, this.currentData()]; + } + currentData() { + const out = []; + if (this.dataWindow) { + const records = this.dataWindow.getData(); + const { keys } = this; + const toClient = this.isTree ? toClientRowTree : toClientRow; + for (const row of records) { + if (row) { + out.push(toClient(row, keys, this.selectedRows)); + } + } + } + return out; + } + enable(requestId) { + this.awaitOperation(requestId, { type: "enable" }); + info == null ? void 0 : info(\`enable: \${this.serverViewportId}\`); + return { + type: ENABLE_VP, + viewPortId: this.serverViewportId + }; + } + disable(requestId) { + this.awaitOperation(requestId, { type: "disable" }); + info == null ? void 0 : info(\`disable: \${this.serverViewportId}\`); + this.suspended = false; + return { + type: DISABLE_VP, + viewPortId: this.serverViewportId + }; + } + columnRequest(requestId, columns) { + this.awaitOperation(requestId, { + type: "columns", + data: columns + }); + debug2 == null ? void 0 : debug2(\`columnRequest: \${columns}\`); + return this.createRequest({ columns }); + } + filterRequest(requestId, dataSourceFilter) { + this.awaitOperation(requestId, { + type: "filter", + data: dataSourceFilter + }); + if (this.useBatchMode) { + this.batchMode = true; + } + const { filter } = dataSourceFilter; + info == null ? void 0 : info(\`filterRequest: \${filter}\`); + return this.createRequest({ filterSpec: { filter } }); + } + setConfig(requestId, config) { + this.awaitOperation(requestId, { type: "config", data: config }); + const { filter, ...remainingConfig } = config; + if (this.useBatchMode) { + this.batchMode = true; + } + debugEnabled2 ? debug2 == null ? void 0 : debug2(\`setConfig \${JSON.stringify(config)}\`) : info == null ? void 0 : info(\`setConfig\`); + return this.createRequest( + { + ...remainingConfig, + filterSpec: typeof (filter == null ? void 0 : filter.filter) === "string" ? { + filter: filter.filter + } : { + filter: "" + } + }, + true + ); + } + aggregateRequest(requestId, aggregations) { + this.awaitOperation(requestId, { type: "aggregate", data: aggregations }); + info == null ? void 0 : info(\`aggregateRequest: \${aggregations}\`); + return this.createRequest({ aggregations }); + } + sortRequest(requestId, sort) { + this.awaitOperation(requestId, { type: "sort", data: sort }); + info == null ? void 0 : info(\`sortRequest: \${JSON.stringify(sort.sortDefs)}\`); + return this.createRequest({ sort }); + } + groupByRequest(requestId, groupBy = EMPTY_GROUPBY) { + var _a; + this.awaitOperation(requestId, { type: "groupBy", data: groupBy }); + if (this.useBatchMode) { + this.batchMode = true; + } + if (!this.isTree) { + (_a = this.dataWindow) == null ? void 0 : _a.clear(); + } + return this.createRequest({ groupBy }); + } + selectRequest(requestId, selected) { + this.selectedRows = selected; + this.awaitOperation(requestId, { type: "selection", data: selected }); + info == null ? void 0 : info(\`selectRequest: \${selected}\`); + return { + type: "SET_SELECTION", + vpId: this.serverViewportId, + selection: expandSelection(selected) + }; + } + removePendingRangeRequest(firstIndex, lastIndex) { + for (let i = this.pendingRangeRequests.length - 1; i >= 0; i--) { + const { from, to } = this.pendingRangeRequests[i]; + let isLast = true; + if (firstIndex >= from && firstIndex < to || lastIndex > from && lastIndex < to) { + if (!isLast) { + console.warn( + "removePendingRangeRequest TABLE_ROWS are not for latest request" + ); + } + this.pendingRangeRequests.splice(i, 1); + break; + } else { + isLast = false; + } + } + } + updateRows(rows) { + var _a, _b, _c; + const [firstRow, lastRow] = getFirstAndLastRows(rows); + if (firstRow && lastRow) { + this.removePendingRangeRequest(firstRow.rowIndex, lastRow.rowIndex); + } + if (rows.length === 1) { + if (firstRow.vpSize === 0 && this.disabled) { + debug2 == null ? void 0 : debug2( + \`ignore a SIZE=0 message on disabled viewport (\${rows.length} rows)\` + ); + return; + } else if (firstRow.updateType === "SIZE") { + this.setLastSizeOnlyUpdateSize(firstRow.vpSize); + } + } + for (const row of rows) { + if (this.isTree && isLeafUpdate(row)) { + continue; + } else { + if (row.updateType === "SIZE" || ((_a = this.dataWindow) == null ? void 0 : _a.rowCount) !== row.vpSize) { + (_b = this.dataWindow) == null ? void 0 : _b.setRowCount(row.vpSize); + this.rowCountChanged = true; + } + if (row.updateType === "U") { + if ((_c = this.dataWindow) == null ? void 0 : _c.setAtIndex(row)) { + this.hasUpdates = true; + if (!this.batchMode) { + this.pendingUpdates.push(row); + } + } + } + } + } + } + // This is called only after new data has been received from server - data + // returned direcly from buffer does not use this. + getClientRows() { + let out = void 0; + let mode = "size-only"; + if (!this.hasUpdates && !this.rowCountChanged) { + return NO_DATA_UPDATE; + } + if (this.hasUpdates) { + const { keys, selectedRows } = this; + const toClient = this.isTree ? toClientRowTree : toClientRow; + if (this.updateThrottleTimer) { + self.clearTimeout(this.updateThrottleTimer); + this.updateThrottleTimer = void 0; + } + if (this.pendingUpdates.length > 0) { + out = []; + mode = "update"; + for (const row of this.pendingUpdates) { + out.push(toClient(row, keys, selectedRows)); + } + this.pendingUpdates.length = 0; + } else { + const records = this.dataWindow.getData(); + if (this.dataWindow.hasAllRowsWithinRange) { + out = []; + mode = "batch"; + for (const row of records) { + out.push(toClient(row, keys, selectedRows)); + } + this.batchMode = false; + } + } + this.hasUpdates = false; + } + if (this.throttleMessage(mode)) { + return NO_DATA_UPDATE; + } else { + return [out, mode]; + } + } + createRequest(params, overWrite = false) { + if (overWrite) { + return { + type: "CHANGE_VP", + viewPortId: this.serverViewportId, + ...params + }; + } else { + return { + type: "CHANGE_VP", + viewPortId: this.serverViewportId, + aggregations: this.aggregations, + columns: this.columns, + sort: this.sort, + groupBy: this.groupBy, + filterSpec: { + filter: this.filter.filter + }, + ...params + }; + } + } +}; +var toClientRow = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { + return [ + rowIndex, + keys.keyFor(rowIndex), + true, + false, + 0, + 0, + rowKey, + isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 + ].concat(data); +}; +var toClientRowTree = ({ rowIndex, rowKey, sel: isSelected, data }, keys, selectedRows) => { + const [depth, isExpanded, , isLeaf, , count, ...rest] = data; + return [ + rowIndex, + keys.keyFor(rowIndex), + isLeaf, + isExpanded, + depth, + count, + rowKey, + isSelected ? getSelectionStatus(selectedRows, rowIndex) : 0 + ].concat(rest); +}; + +// src/server-proxy/server-proxy.ts +var _requestId = 1; +var { debug: debug3, debugEnabled: debugEnabled3, error: error2, info: info2, infoEnabled: infoEnabled2, warn: warn2 } = logger("server-proxy"); +var nextRequestId = () => \`\${_requestId++}\`; +var DEFAULT_OPTIONS = {}; +var isActiveViewport = (viewPort) => viewPort.disabled !== true && viewPort.suspended !== true; +var NO_ACTION = { + type: "NO_ACTION" +}; +var addTitleToLinks = (links, serverViewportId, label) => links.map( + (link) => link.parentVpId === serverViewportId ? { ...link, label } : link +); +function addLabelsToLinks(links, viewports) { + return links.map((linkDescriptor) => { + const { parentVpId } = linkDescriptor; + const viewport = viewports.get(parentVpId); + if (viewport) { + return { + ...linkDescriptor, + parentClientVpId: viewport.clientViewportId, + label: viewport.title + }; + } else { + throw Error("addLabelsToLinks viewport not found"); + } + }); +} +var ServerProxy = class { + constructor(connection, callback) { + this.authToken = ""; + this.user = "user"; + this.pendingRequests = /* @__PURE__ */ new Map(); + this.queuedRequests = []; + this.cachedTableMetaRequests = /* @__PURE__ */ new Map(); + this.cachedTableSchemas = /* @__PURE__ */ new Map(); + this.connection = connection; + this.postMessageToClient = callback; + this.viewports = /* @__PURE__ */ new Map(); + this.mapClientToServerViewport = /* @__PURE__ */ new Map(); + } + async reconnect() { + await this.login(this.authToken); + const [activeViewports, inactiveViewports] = partition( + Array.from(this.viewports.values()), + isActiveViewport + ); + this.viewports.clear(); + this.mapClientToServerViewport.clear(); + const reconnectViewports = (viewports) => { + viewports.forEach((viewport) => { + const { clientViewportId } = viewport; + this.viewports.set(clientViewportId, viewport); + this.sendMessageToServer(viewport.subscribe(), clientViewportId); + }); + }; + reconnectViewports(activeViewports); + setTimeout(() => { + reconnectViewports(inactiveViewports); + }, 2e3); + } + async login(authToken, user = "user") { + if (authToken) { + this.authToken = authToken; + this.user = user; + return new Promise((resolve, reject) => { + this.sendMessageToServer( + { type: LOGIN, token: this.authToken, user }, + "" + ); + this.pendingLogin = { resolve, reject }; + }); + } else if (this.authToken === "") { + error2("login, cannot login until auth token has been obtained"); + } + } + subscribe(message) { + if (!this.mapClientToServerViewport.has(message.viewport)) { + const pendingTableSchema = this.getTableMeta(message.table); + const viewport = new Viewport(message, this.postMessageToClient); + this.viewports.set(message.viewport, viewport); + const pendingSubscription = this.awaitResponseToMessage( + viewport.subscribe(), + message.viewport + ); + const awaitPendingReponses = Promise.all([ + pendingSubscription, + pendingTableSchema + ]); + awaitPendingReponses.then(([subscribeResponse, tableSchema]) => { + const { viewPortId: serverViewportId } = subscribeResponse; + const { status: previousViewportStatus } = viewport; + if (message.viewport !== serverViewportId) { + this.viewports.delete(message.viewport); + this.viewports.set(serverViewportId, viewport); + } + this.mapClientToServerViewport.set(message.viewport, serverViewportId); + const clientResponse = viewport.handleSubscribed( + subscribeResponse, + tableSchema + ); + if (clientResponse) { + this.postMessageToClient(clientResponse); + if (debugEnabled3) { + debug3( + \`post DataSourceSubscribedMessage to client: \${JSON.stringify( + clientResponse + )}\` + ); + } + } + if (viewport.disabled) { + this.disableViewport(viewport); + } + if (this.queuedRequests.length > 0) { + this.processQueuedRequests(); + } + if (previousViewportStatus === "subscribing" && // A session table will never have Visual Links, nor Context Menus + !isSessionTable(viewport.table)) { + this.sendMessageToServer({ + type: GET_VP_VISUAL_LINKS, + vpId: serverViewportId + }); + this.sendMessageToServer({ + type: GET_VIEW_PORT_MENUS, + vpId: serverViewportId + }); + Array.from(this.viewports.entries()).filter( + ([id, { disabled }]) => id !== serverViewportId && !disabled + ).forEach(([vpId]) => { + this.sendMessageToServer({ + type: GET_VP_VISUAL_LINKS, + vpId + }); + }); + } + }); + } else { + error2(\`spurious subscribe call \${message.viewport}\`); + } + } + processQueuedRequests() { + const messageTypesProcessed = {}; + while (this.queuedRequests.length) { + const queuedRequest = this.queuedRequests.pop(); + if (queuedRequest) { + const { clientViewportId, message, requestId } = queuedRequest; + if (message.type === "CHANGE_VP_RANGE") { + if (messageTypesProcessed.CHANGE_VP_RANGE) { + continue; + } + messageTypesProcessed.CHANGE_VP_RANGE = true; + const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + this.sendMessageToServer( + { + ...message, + viewPortId: serverViewportId + }, + requestId + ); + } + } + } + } + } + unsubscribe(clientViewportId) { + const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + info2 == null ? void 0 : info2( + \`Unsubscribe Message (Client to Server): + \${serverViewportId}\` + ); + this.sendMessageToServer({ + type: REMOVE_VP, + viewPortId: serverViewportId + }); + } else { + error2( + \`failed to unsubscribe client viewport \${clientViewportId}, viewport not found\` + ); + } + } + getViewportForClient(clientViewportId, throws = true) { + const serverViewportId = this.mapClientToServerViewport.get(clientViewportId); + if (serverViewportId) { + const viewport = this.viewports.get(serverViewportId); + if (viewport) { + return viewport; + } else if (throws) { + throw Error( + \`Viewport not found for client viewport \${clientViewportId}\` + ); + } else { + return null; + } + } else if (this.viewports.has(clientViewportId)) { + return this.viewports.get(clientViewportId); + } else if (throws) { + throw Error( + \`Viewport server id not found for client viewport \${clientViewportId}\` + ); + } else { + return null; + } + } + /**********************************************************************/ + /* Handle messages from client */ + /**********************************************************************/ + setViewRange(viewport, message) { + const requestId = nextRequestId(); + const [serverRequest, rows, debounceRequest] = viewport.rangeRequest( + requestId, + message.range + ); + info2 == null ? void 0 : info2(\`setViewRange \${message.range.from} - \${message.range.to}\`); + if (serverRequest) { + if (true) { + info2 == null ? void 0 : info2( + \`CHANGE_VP_RANGE [\${message.range.from}-\${message.range.to}] => [\${serverRequest.from}-\${serverRequest.to}]\` + ); + } + const sentToServer = this.sendIfReady( + serverRequest, + requestId, + viewport.status === "subscribed" + ); + if (!sentToServer) { + this.queuedRequests.push({ + clientViewportId: message.viewport, + message: serverRequest, + requestId + }); + } + } + if (rows) { + info2 == null ? void 0 : info2(\`setViewRange \${rows.length} rows returned from cache\`); + this.postMessageToClient({ + mode: "batch", + type: "viewport-update", + clientViewportId: viewport.clientViewportId, + rows + }); + } else if (debounceRequest) { + this.postMessageToClient(debounceRequest); + } + } + setConfig(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.setConfig(requestId, message.config); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + aggregate(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.aggregateRequest(requestId, message.aggregations); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + sort(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.sortRequest(requestId, message.sort); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + groupBy(viewport, message) { + const requestId = nextRequestId(); + const request = viewport.groupByRequest(requestId, message.groupBy); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + filter(viewport, message) { + const requestId = nextRequestId(); + const { filter } = message; + const request = viewport.filterRequest(requestId, filter); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + setColumns(viewport, message) { + const requestId = nextRequestId(); + const { columns } = message; + const request = viewport.columnRequest(requestId, columns); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + setTitle(viewport, message) { + if (viewport) { + viewport.title = message.title; + this.updateTitleOnVisualLinks(viewport); + } + } + select(viewport, message) { + const requestId = nextRequestId(); + const { selected } = message; + const request = viewport.selectRequest(requestId, selected); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + disableViewport(viewport) { + const requestId = nextRequestId(); + const request = viewport.disable(requestId); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + enableViewport(viewport) { + if (viewport.disabled) { + const requestId = nextRequestId(); + const request = viewport.enable(requestId); + this.sendIfReady(request, requestId, viewport.status === "subscribed"); + } + } + suspendViewport(viewport) { + viewport.suspend(); + viewport.suspendTimer = setTimeout(() => { + info2 == null ? void 0 : info2("suspendTimer expired, escalate suspend to disable"); + this.disableViewport(viewport); + }, 3e3); + } + resumeViewport(viewport) { + if (viewport.suspendTimer) { + debug3 == null ? void 0 : debug3("clear suspend timer"); + clearTimeout(viewport.suspendTimer); + viewport.suspendTimer = null; + } + const [size, rows] = viewport.resume(); + debug3 == null ? void 0 : debug3(\`resumeViewport size \${size}, \${rows.length} rows sent to client\`); + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode: "batch", + rows, + size, + type: "viewport-update" + }); + } + openTreeNode(viewport, message) { + if (viewport.serverViewportId) { + const requestId = nextRequestId(); + this.sendIfReady( + viewport.openTreeNode(requestId, message), + requestId, + viewport.status === "subscribed" + ); + } + } + closeTreeNode(viewport, message) { + if (viewport.serverViewportId) { + const requestId = nextRequestId(); + this.sendIfReady( + viewport.closeTreeNode(requestId, message), + requestId, + viewport.status === "subscribed" + ); + } + } + createLink(viewport, message) { + const { parentClientVpId, parentColumnName, childColumnName } = message; + const requestId = nextRequestId(); + const parentVpId = this.mapClientToServerViewport.get(parentClientVpId); + if (parentVpId) { + const request = viewport.createLink( + requestId, + childColumnName, + parentVpId, + parentColumnName + ); + this.sendMessageToServer(request, requestId); + } else { + error2("ServerProxy unable to create link, viewport not found"); + } + } + removeLink(viewport) { + const requestId = nextRequestId(); + const request = viewport.removeLink(requestId); + this.sendMessageToServer(request, requestId); + } + updateTitleOnVisualLinks(viewport) { + var _a; + const { serverViewportId, title } = viewport; + for (const vp of this.viewports.values()) { + if (vp !== viewport && vp.links && serverViewportId && title) { + if ((_a = vp.links) == null ? void 0 : _a.some((link) => link.parentVpId === serverViewportId)) { + const [messageToClient] = vp.setLinks( + addTitleToLinks(vp.links, serverViewportId, title) + ); + this.postMessageToClient(messageToClient); + } + } + } + } + removeViewportFromVisualLinks(serverViewportId) { + var _a; + for (const vp of this.viewports.values()) { + if ((_a = vp.links) == null ? void 0 : _a.some(({ parentVpId }) => parentVpId === serverViewportId)) { + const [messageToClient] = vp.setLinks( + vp.links.filter(({ parentVpId }) => parentVpId !== serverViewportId) + ); + this.postMessageToClient(messageToClient); + } + } + } + menuRpcCall(message) { + const viewport = this.getViewportForClient(message.vpId, false); + if (viewport == null ? void 0 : viewport.serverViewportId) { + const [requestId, rpcRequest] = stripRequestId(message); + this.sendMessageToServer( + { + ...rpcRequest, + vpId: viewport.serverViewportId + }, + requestId + ); + } + } + viewportRpcCall(message) { + const viewport = this.getViewportForClient(message.vpId, false); + if (viewport == null ? void 0 : viewport.serverViewportId) { + const [requestId, rpcRequest] = stripRequestId(message); + this.sendMessageToServer( + { + ...rpcRequest, + vpId: viewport.serverViewportId, + namedParams: {} + }, + requestId + ); + } + } + rpcCall(message) { + const [requestId, rpcRequest] = stripRequestId(message); + const module = getRpcServiceModule(rpcRequest.service); + this.sendMessageToServer(rpcRequest, requestId, { module }); + } + handleMessageFromClient(message) { + var _a; + if (isViewporttMessage(message)) { + if (message.type === "disable") { + const viewport = this.getViewportForClient(message.viewport, false); + if (viewport !== null) { + return this.disableViewport(viewport); + } else { + return; + } + } else { + const viewport = this.getViewportForClient(message.viewport); + switch (message.type) { + case "setViewRange": + return this.setViewRange(viewport, message); + case "config": + return this.setConfig(viewport, message); + case "aggregate": + return this.aggregate(viewport, message); + case "sort": + return this.sort(viewport, message); + case "groupBy": + return this.groupBy(viewport, message); + case "filter": + return this.filter(viewport, message); + case "select": + return this.select(viewport, message); + case "suspend": + return this.suspendViewport(viewport); + case "resume": + return this.resumeViewport(viewport); + case "enable": + return this.enableViewport(viewport); + case "openTreeNode": + return this.openTreeNode(viewport, message); + case "closeTreeNode": + return this.closeTreeNode(viewport, message); + case "createLink": + return this.createLink(viewport, message); + case "removeLink": + return this.removeLink(viewport); + case "setColumns": + return this.setColumns(viewport, message); + case "setTitle": + return this.setTitle(viewport, message); + default: + } + } + } else if (isVuuRpcRequest(message)) { + return this.viewportRpcCall( + message + ); + } else if (isVuuMenuRpcRequest(message)) { + return this.menuRpcCall(message); + } else { + const { type, requestId } = message; + switch (type) { + case "GET_TABLE_LIST": { + (_a = this.tableList) != null ? _a : this.tableList = this.awaitResponseToMessage( + { type }, + requestId + ); + this.tableList.then((response) => { + this.postMessageToClient({ + type: "TABLE_LIST_RESP", + tables: response.tables, + requestId + }); + }); + return; + } + case "GET_TABLE_META": { + this.getTableMeta(message.table, requestId).then((tableSchema) => { + if (tableSchema) { + this.postMessageToClient({ + type: "TABLE_META_RESP", + tableSchema, + requestId + }); + } + }); + return; + } + case "RPC_CALL": + return this.rpcCall(message); + default: + } + } + error2( + \`Vuu ServerProxy Unexpected message from client \${JSON.stringify( + message + )}\` + ); + } + getTableMeta(table, requestId = nextRequestId()) { + if (isSessionTable(table)) { + return Promise.resolve(void 0); + } + const key = \`\${table.module}:\${table.table}\`; + let tableMetaRequest = this.cachedTableMetaRequests.get(key); + if (!tableMetaRequest) { + tableMetaRequest = this.awaitResponseToMessage( + { type: "GET_TABLE_META", table }, + requestId + ); + this.cachedTableMetaRequests.set(key, tableMetaRequest); + } + return tableMetaRequest == null ? void 0 : tableMetaRequest.then((response) => this.cacheTableMeta(response)); + } + awaitResponseToMessage(message, requestId = nextRequestId()) { + return new Promise((resolve, reject) => { + this.sendMessageToServer(message, requestId); + this.pendingRequests.set(requestId, { reject, resolve }); + }); + } + sendIfReady(message, requestId, isReady = true) { + if (isReady) { + this.sendMessageToServer(message, requestId); + } + return isReady; + } + sendMessageToServer(body, requestId = \`\${_requestId++}\`, options = DEFAULT_OPTIONS) { + const { module = "CORE" } = options; + if (this.authToken) { + this.connection.send({ + requestId, + sessionId: this.sessionId, + token: this.authToken, + user: this.user, + module, + body + }); + } + } + handleMessageFromServer(message) { + var _a, _b, _c; + const { body, requestId, sessionId } = message; + const pendingRequest = this.pendingRequests.get(requestId); + if (pendingRequest) { + const { resolve } = pendingRequest; + this.pendingRequests.delete(requestId); + resolve(body); + return; + } + const { viewports } = this; + switch (body.type) { + case HB: + this.sendMessageToServer( + { type: HB_RESP, ts: +/* @__PURE__ */ new Date() }, + "NA" + ); + break; + case "LOGIN_SUCCESS": + if (sessionId) { + this.sessionId = sessionId; + (_a = this.pendingLogin) == null ? void 0 : _a.resolve(sessionId); + this.pendingLogin = void 0; + } else { + throw Error("LOGIN_SUCCESS did not provide sessionId"); + } + break; + case "REMOVE_VP_SUCCESS": + { + const viewport = viewports.get(body.viewPortId); + if (viewport) { + this.mapClientToServerViewport.delete(viewport.clientViewportId); + viewports.delete(body.viewPortId); + this.removeViewportFromVisualLinks(body.viewPortId); + } + } + break; + case SET_SELECTION_SUCCESS: + { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + viewport.completeOperation(requestId); + } + } + break; + case CHANGE_VP_SUCCESS: + case DISABLE_VP_SUCCESS: + if (viewports.has(body.viewPortId)) { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const response = viewport.completeOperation(requestId); + if (response !== void 0) { + this.postMessageToClient(response); + if (debugEnabled3) { + debug3(\`postMessageToClient \${JSON.stringify(response)}\`); + } + } + } + } + break; + case ENABLE_VP_SUCCESS: + { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const response = viewport.completeOperation(requestId); + if (response) { + this.postMessageToClient(response); + const [size, rows] = viewport.resume(); + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode: "batch", + rows, + size, + type: "viewport-update" + }); + } + } + } + break; + case "TABLE_ROW": + { + const viewportRowMap = groupRowsByViewport(body.rows); + if (debugEnabled3) { + const [firstRow, secondRow] = body.rows; + if (body.rows.length === 0) { + debug3("handleMessageFromServer TABLE_ROW 0 rows"); + } else if ((firstRow == null ? void 0 : firstRow.rowIndex) === -1) { + if (body.rows.length === 1) { + if (firstRow.updateType === "SIZE") { + debug3( + \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE ONLY \${firstRow.vpSize}\` + ); + } else { + debug3( + \`handleMessageFromServer [\${firstRow.viewPortId}] TABLE_ROW SIZE \${firstRow.vpSize} rowIdx \${firstRow.rowIndex}\` + ); + } + } else { + debug3( + \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows, SIZE \${firstRow.vpSize}, [\${secondRow == null ? void 0 : secondRow.rowIndex}] - [\${(_b = body.rows[body.rows.length - 1]) == null ? void 0 : _b.rowIndex}]\` + ); + } + } else { + debug3( + \`handleMessageFromServer TABLE_ROW \${body.rows.length} rows [\${firstRow == null ? void 0 : firstRow.rowIndex}] - [\${(_c = body.rows[body.rows.length - 1]) == null ? void 0 : _c.rowIndex}]\` + ); + } + } + for (const [viewportId, rows] of Object.entries(viewportRowMap)) { + const viewport = viewports.get(viewportId); + if (viewport) { + viewport.updateRows(rows); + } else { + warn2 == null ? void 0 : warn2( + \`TABLE_ROW message received for non registered viewport \${viewportId}\` + ); + } + } + this.processUpdates(); + } + break; + case "CHANGE_VP_RANGE_SUCCESS": + { + const viewport = this.viewports.get(body.viewPortId); + if (viewport) { + const { from, to } = body; + if (true) { + info2 == null ? void 0 : info2(\`CHANGE_VP_RANGE_SUCCESS \${from} - \${to}\`); + } + viewport.completeOperation(requestId, from, to); + } + } + break; + case OPEN_TREE_SUCCESS: + case CLOSE_TREE_SUCCESS: + break; + case "CREATE_VISUAL_LINK_SUCCESS": + { + const viewport = this.viewports.get(body.childVpId); + const parentViewport = this.viewports.get(body.parentVpId); + if (viewport && parentViewport) { + const { childColumnName, parentColumnName } = body; + const response = viewport.completeOperation( + requestId, + childColumnName, + parentViewport.clientViewportId, + parentColumnName + ); + if (response) { + this.postMessageToClient(response); + } + } + } + break; + case "REMOVE_VISUAL_LINK_SUCCESS": + { + const viewport = this.viewports.get(body.childVpId); + if (viewport) { + const response = viewport.completeOperation( + requestId + ); + if (response) { + this.postMessageToClient(response); + } + } + } + break; + case "VP_VISUAL_LINKS_RESP": + { + const activeLinkDescriptors = this.getActiveLinks(body.links); + const viewport = this.viewports.get(body.vpId); + if (activeLinkDescriptors.length && viewport) { + const linkDescriptorsWithLabels = addLabelsToLinks( + activeLinkDescriptors, + this.viewports + ); + const [clientMessage, pendingLink] = viewport.setLinks( + linkDescriptorsWithLabels + ); + this.postMessageToClient(clientMessage); + if (pendingLink) { + const { link, parentClientVpId } = pendingLink; + const requestId2 = nextRequestId(); + const serverViewportId = this.mapClientToServerViewport.get(parentClientVpId); + if (serverViewportId) { + const message2 = viewport.createLink( + requestId2, + link.fromColumn, + serverViewportId, + link.toColumn + ); + this.sendMessageToServer(message2, requestId2); + } + } + } + } + break; + case "VIEW_PORT_MENUS_RESP": + if (body.menu.name) { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + const clientMessage = viewport.setMenu(body.menu); + this.postMessageToClient(clientMessage); + } + } + break; + case "VP_EDIT_RPC_RESPONSE": + { + this.postMessageToClient({ + action: body.action, + requestId, + rpcName: body.rpcName, + type: "VP_EDIT_RPC_RESPONSE" + }); + } + break; + case "VP_EDIT_RPC_REJECT": + { + const viewport = this.viewports.get(body.vpId); + if (viewport) { + this.postMessageToClient({ + requestId, + type: "VP_EDIT_RPC_REJECT", + error: body.error + }); + } + } + break; + case "VIEW_PORT_MENU_REJ": { + console.log(\`send menu error back to client\`); + const { error: error4, rpcName, vpId } = body; + const viewport = this.viewports.get(vpId); + if (viewport) { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + error: error4, + rpcName, + type: "VIEW_PORT_MENU_REJ", + requestId + }); + } + break; + } + case "VIEW_PORT_MENU_RESP": + { + if (isSessionTableActionMessage(body)) { + const { action, rpcName } = body; + this.awaitResponseToMessage({ + type: "GET_TABLE_META", + table: action.table + }).then((response) => { + const tableSchema = createSchemaFromTableMetadata( + response + ); + this.postMessageToClient({ + rpcName, + type: "VIEW_PORT_MENU_RESP", + action: { + ...action, + tableSchema + }, + tableAlreadyOpen: this.isTableOpen(action.table), + requestId + }); + }); + } else { + const { action } = body; + this.postMessageToClient({ + type: "VIEW_PORT_MENU_RESP", + action: action || NO_ACTION, + tableAlreadyOpen: action !== null && this.isTableOpen(action.table), + requestId + }); + } + } + break; + case "RPC_RESP": + { + const { method, result } = body; + this.postMessageToClient({ + type: "RPC_RESP", + method, + result, + requestId + }); + } + break; + case "VIEW_PORT_RPC_REPONSE": + { + const { method, action } = body; + this.postMessageToClient({ + type: "VIEW_PORT_RPC_RESPONSE", + rpcName: method, + action, + requestId + }); + } + break; + case "ERROR": + error2(body.msg); + break; + default: + infoEnabled2 && info2(\`handleMessageFromServer \${body["type"]}.\`); + } + } + cacheTableMeta(messageBody) { + const { module, table } = messageBody.table; + const key = \`\${module}:\${table}\`; + let tableSchema = this.cachedTableSchemas.get(key); + if (!tableSchema) { + tableSchema = createSchemaFromTableMetadata(messageBody); + this.cachedTableSchemas.set(key, tableSchema); + } + return tableSchema; + } + isTableOpen(table) { + if (table) { + const tableName = table.table; + for (const viewport of this.viewports.values()) { + if (!viewport.suspended && viewport.table.table === tableName) { + return true; + } + } + } + } + // Eliminate links to suspended viewports + getActiveLinks(linkDescriptors) { + return linkDescriptors.filter((linkDescriptor) => { + const viewport = this.viewports.get(linkDescriptor.parentVpId); + return viewport && !viewport.suspended; + }); + } + processUpdates() { + this.viewports.forEach((viewport) => { + var _a; + if (viewport.hasUpdatesToProcess) { + const result = viewport.getClientRows(); + if (result !== NO_DATA_UPDATE) { + const [rows, mode] = result; + const size = viewport.getNewRowCount(); + if (size !== void 0 || rows && rows.length > 0) { + debugEnabled3 && debug3( + \`postMessageToClient #\${viewport.clientViewportId} viewport-update \${mode}, \${(_a = rows == null ? void 0 : rows.length) != null ? _a : "no"} rows, size \${size}\` + ); + if (mode) { + this.postMessageToClient({ + clientViewportId: viewport.clientViewportId, + mode, + rows, + size, + type: "viewport-update" + }); + } + } + } + } + }); + } +}; + +// src/websocket-connection.ts +var { debug: debug4, debugEnabled: debugEnabled4, error: error3, info: info3, infoEnabled: infoEnabled3, warn: warn3 } = logger( + "websocket-connection" +); +var WS = "ws"; +var isWebsocketUrl = (url) => url.startsWith(WS + "://") || url.startsWith(WS + "s://"); +var connectionAttemptStatus = {}; +var setWebsocket = Symbol("setWebsocket"); +var connectionCallback = Symbol("connectionCallback"); +async function connect(connectionString, protocol, callback, retryLimitDisconnect = 10, retryLimitStartup = 5) { + connectionAttemptStatus[connectionString] = { + status: "connecting", + connect: { + allowed: retryLimitStartup, + remaining: retryLimitStartup + }, + reconnect: { + allowed: retryLimitDisconnect, + remaining: retryLimitDisconnect + } + }; + return makeConnection(connectionString, protocol, callback); +} +async function reconnect(connection) { + throw Error("connection broken"); +} +async function makeConnection(url, protocol, callback, connection) { + const { + status: currentStatus, + connect: connectStatus, + reconnect: reconnectStatus + } = connectionAttemptStatus[url]; + const trackedStatus = currentStatus === "connecting" ? connectStatus : reconnectStatus; + try { + callback({ type: "connection-status", status: "connecting" }); + const reconnecting = typeof connection !== "undefined"; + const ws = await createWebsocket(url, protocol); + console.info( + "%c\u26A1 %cconnected", + "font-size: 24px;color: green;font-weight: bold;", + "color:green; font-size: 14px;" + ); + if (connection !== void 0) { + connection[setWebsocket](ws); + } + const websocketConnection = connection != null ? connection : new WebsocketConnection(ws, url, protocol, callback); + const status = reconnecting ? "reconnected" : "connection-open-awaiting-session"; + callback({ type: "connection-status", status }); + websocketConnection.status = status; + trackedStatus.remaining = trackedStatus.allowed; + return websocketConnection; + } catch (err) { + const retry = --trackedStatus.remaining > 0; + callback({ + type: "connection-status", + status: "disconnected", + reason: "failed to connect", + retry + }); + if (retry) { + return makeConnectionIn(url, protocol, callback, connection, 2e3); + } else { + throw Error("Failed to establish connection"); + } + } +} +var makeConnectionIn = (url, protocol, callback, connection, delay) => new Promise((resolve) => { + setTimeout(() => { + resolve(makeConnection(url, protocol, callback, connection)); + }, delay); +}); +var createWebsocket = (connectionString, protocol) => new Promise((resolve, reject) => { + const websocketUrl = isWebsocketUrl(connectionString) ? connectionString : \`wss://\${connectionString}\`; + if (infoEnabled3 && protocol !== void 0) { + info3(\`WebSocket Protocol \${protocol == null ? void 0 : protocol.toString()}\`); + } + const ws = new WebSocket(websocketUrl, protocol); + ws.onopen = () => resolve(ws); + ws.onerror = (evt) => reject(evt); +}); +var closeWarn = () => { + warn3 == null ? void 0 : warn3(\`Connection cannot be closed, socket not yet opened\`); +}; +var sendWarn = (msg) => { + warn3 == null ? void 0 : warn3(\`Message cannot be sent, socket closed \${msg.body.type}\`); +}; +var parseMessage = (message) => { + try { + return JSON.parse(message); + } catch (e) { + throw Error(\`Error parsing JSON response from server \${message}\`); + } +}; +var WebsocketConnection = class { + constructor(ws, url, protocol, callback) { + this.close = closeWarn; + this.requiresLogin = true; + this.send = sendWarn; + this.status = "ready"; + this.messagesCount = 0; + this.connectionMetricsInterval = null; + this.handleWebsocketMessage = (evt) => { + const vuuMessageFromServer = parseMessage(evt.data); + this.messagesCount += 1; + if (true) { + if (debugEnabled4 && vuuMessageFromServer.body.type !== "HB") { + debug4 == null ? void 0 : debug4(\`<<< \${vuuMessageFromServer.body.type}\`); + } + } + this[connectionCallback](vuuMessageFromServer); + }; + this.url = url; + this.protocol = protocol; + this[connectionCallback] = callback; + this[setWebsocket](ws); + } + reconnect() { + reconnect(this); + } + [(connectionCallback, setWebsocket)](ws) { + const callback = this[connectionCallback]; + ws.onmessage = (evt) => { + this.status = "connected"; + ws.onmessage = this.handleWebsocketMessage; + this.handleWebsocketMessage(evt); + }; + this.connectionMetricsInterval = setInterval(() => { + callback({ + type: "connection-metrics", + messagesLength: this.messagesCount + }); + this.messagesCount = 0; + }, 2e3); + ws.onerror = () => { + error3(\`\u26A1 connection error\`); + callback({ + type: "connection-status", + status: "disconnected", + reason: "error" + }); + if (this.connectionMetricsInterval) { + clearInterval(this.connectionMetricsInterval); + this.connectionMetricsInterval = null; + } + if (this.status === "connection-open-awaiting-session") { + error3( + \`Websocket connection lost before Vuu session established, check websocket configuration\` + ); + } else if (this.status !== "closed") { + reconnect(this); + this.send = queue; + } + }; + ws.onclose = () => { + info3 == null ? void 0 : info3(\`\u26A1 connection close\`); + callback({ + type: "connection-status", + status: "disconnected", + reason: "close" + }); + if (this.connectionMetricsInterval) { + clearInterval(this.connectionMetricsInterval); + this.connectionMetricsInterval = null; + } + if (this.status !== "closed") { + reconnect(this); + this.send = queue; + } + }; + const send = (msg) => { + if (true) { + if (debugEnabled4 && msg.body.type !== "HB_RESP") { + debug4 == null ? void 0 : debug4(\`>>> \${msg.body.type}\`); + } + } + ws.send(JSON.stringify(msg)); + }; + const queue = (msg) => { + info3 == null ? void 0 : info3(\`TODO queue message until websocket reconnected \${msg.body.type}\`); + }; + this.send = send; + this.close = () => { + this.status = "closed"; + ws.close(); + this.close = closeWarn; + this.send = sendWarn; + info3 == null ? void 0 : info3("close websocket"); + }; + } +}; + +// src/worker.ts +var server; +var { info: info4, infoEnabled: infoEnabled4 } = logger("worker"); +async function connectToServer(url, protocol, token, username, onConnectionStatusChange, retryLimitDisconnect, retryLimitStartup) { + const connection = await connect( + url, + protocol, + // if this was called during connect, we would get a ReferenceError, but it will + // never be called until subscriptions have been made, so this is safe. + //TODO do we need to listen in to the connection messages here so we can lock back in, in the event of a reconnenct ? + (msg) => { + if (isConnectionQualityMetrics(msg)) { + postMessage({ type: "connection-metrics", messages: msg }); + } else if (isConnectionStatusMessage(msg)) { + onConnectionStatusChange(msg); + if (msg.status === "reconnected") { + server.reconnect(); + } + } else { + server.handleMessageFromServer(msg); + } + }, + retryLimitDisconnect, + retryLimitStartup + ); + server = new ServerProxy(connection, (msg) => sendMessageToClient(msg)); + if (connection.requiresLogin) { + await server.login(token, username); + } +} +function sendMessageToClient(message) { + postMessage(message); +} +var handleMessageFromClient = async ({ + data: message +}) => { + switch (message.type) { + case "connect": + await connectToServer( + message.url, + message.protocol, + message.token, + message.username, + postMessage, + message.retryLimitDisconnect, + message.retryLimitStartup + ); + postMessage({ type: "connected" }); + break; + case "subscribe": + infoEnabled4 && info4(\`client subscribe: \${JSON.stringify(message)}\`); + server.subscribe(message); + break; + case "unsubscribe": + infoEnabled4 && info4(\`client unsubscribe: \${JSON.stringify(message)}\`); + server.unsubscribe(message.viewport); + break; + default: + infoEnabled4 && info4(\`client message: \${JSON.stringify(message)}\`); + server.handleMessageFromClient(message); + } +}; +self.addEventListener("message", handleMessageFromClient); +postMessage({ type: "ready" }); `; \ No newline at end of file diff --git a/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts b/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts index 692dbbe45..cc12240b8 100644 --- a/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data-remote/test/server-proxy.test.ts @@ -18,8 +18,8 @@ import { updateTableRow, createSubscription, } from "./test-utils"; -import { DataSourceDataMessage, DataSourceEnabledMessage } from "../src"; import { VuuRow } from "@finos/vuu-protocol-types"; +import { DataSourceDataMessage } from "@finos/vuu-data-types"; const SERVER_MESSAGE_CONSTANTS = { module: "CORE", @@ -270,8 +270,8 @@ describe("ServerProxy", () => { [7,7,true,false,0,0,"key-07",0,"key-07","name 07",1007,true], [8,8,true,false,0,0,"key-08",0,"key-08","name 08",1008,true], [9,9,true,false,0,0,"key-09",0,"key-09","name 09",1009,true], - [10,1,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], - [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [10,0,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], + [11,1,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], ], }); }); @@ -323,16 +323,16 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [20,9,true,false,0,0,"key-20",0,"key-20","name 20",1020,true,], - [21,8,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], - [22,7,true,false,0,0,"key-22",0,"key-22","name 22",1022,true], - [23,6,true,false,0,0,"key-23",0,"key-23","name 23",1023,true], - [24,5,true,false,0,0,"key-24",0,"key-24","name 24",1024,true], - [25,4,true,false,0,0,"key-25",0,"key-25","name 25",1025,true], - [26,3,true,false,0,0,"key-26",0,"key-26","name 26",1026,true], - [27,2,true,false,0,0,"key-27",0,"key-27","name 27",1027,true], - [28,1,true,false,0,0,"key-28",0,"key-28","name 28",1028,true], - [29,0,true,false,0,0,"key-29",0,"key-29","name 29",1029,true,], + [20,0,true,false,0,0,"key-20",0,"key-20","name 20",1020,true,], + [21,1,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], + [22,2,true,false,0,0,"key-22",0,"key-22","name 22",1022,true], + [23,3,true,false,0,0,"key-23",0,"key-23","name 23",1023,true], + [24,4,true,false,0,0,"key-24",0,"key-24","name 24",1024,true], + [25,5,true,false,0,0,"key-25",0,"key-25","name 25",1025,true], + [26,6,true,false,0,0,"key-26",0,"key-26","name 26",1026,true], + [27,7,true,false,0,0,"key-27",0,"key-27","name 27",1027,true], + [28,8,true,false,0,0,"key-28",0,"key-28","name 28",1028,true], + [29,9,true,false,0,0,"key-29",0,"key-29","name 29",1029,true,], ], }); }); @@ -499,15 +499,15 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [11,8,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], - [12,7,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], - [13,6,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,5,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [12,1,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [13,2,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], + [14,3,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], [15,4,true,false,0,0,"key-15",0,"key-15","name 15",1015,true,], - [16,3,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,2,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], - [18,1,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], - [19,0,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], + [16,5,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], + [17,6,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [18,7,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], + [19,8,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], ], }); }); @@ -707,26 +707,26 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [4975,19,true,false,0,0,"key-75",0,"key-75","name 75",5975,true], - [4976,18,true,false,0,0,"key-76",0,"key-76","name 76",5976,true], - [4977,17,true,false,0,0,"key-77",0,"key-77","name 77",5977,true], - [4978,16,true,false,0,0,"key-78",0,"key-78","name 78",5978,true], - [4979,15,true,false,0,0,"key-79",0,"key-79","name 79",5979,true], - [4980,14,true,false,0,0,"key-80",0,"key-80","name 80",5980,true], - [4981,13,true,false,0,0,"key-81",0,"key-81","name 81",5981,true], - [4982,12,true,false,0,0,"key-82",0,"key-82","name 82",5982,true], - [4983,11,true,false,0,0,"key-83",0,"key-83","name 83",5983,true], - [4984,10,true,false,0,0,"key-84",0,"key-84","name 84",5984,true], - [4985,9,true,false,0,0,"key-85",0,"key-85","name 85",5985,true], - [4986,8,true,false,0,0,"key-86",0,"key-86","name 86",5986,true], - [4987,7,true,false,0,0,"key-87",0,"key-87","name 87",5987,true], - [4988,6,true,false,0,0,"key-88",0,"key-88","name 88",5988,true], - [4989,5,true,false,0,0,"key-89",0,"key-89","name 89",5989,true], - [4990,4,true,false,0,0,"key-90",0,"key-90","name 90",5990,true], - [4991,3,true,false,0,0,"key-91",0,"key-91","name 91",5991,true], - [4992,2,true,false,0,0,"key-92",0,"key-92","name 92",5992,true], - [4993,1,true,false,0,0,"key-93",0,"key-93","name 93",5993,true], - [4994,0,true,false,0,0,"key-94",0,"key-94","name 94",5994,true], + [4975,0,true,false,0,0,"key-75",0,"key-75","name 75",5975,true], + [4976,1,true,false,0,0,"key-76",0,"key-76","name 76",5976,true], + [4977,2,true,false,0,0,"key-77",0,"key-77","name 77",5977,true], + [4978,3,true,false,0,0,"key-78",0,"key-78","name 78",5978,true], + [4979,4,true,false,0,0,"key-79",0,"key-79","name 79",5979,true], + [4980,5,true,false,0,0,"key-80",0,"key-80","name 80",5980,true], + [4981,6,true,false,0,0,"key-81",0,"key-81","name 81",5981,true], + [4982,7,true,false,0,0,"key-82",0,"key-82","name 82",5982,true], + [4983,8,true,false,0,0,"key-83",0,"key-83","name 83",5983,true], + [4984,9,true,false,0,0,"key-84",0,"key-84","name 84",5984,true], + [4985,10,true,false,0,0,"key-85",0,"key-85","name 85",5985,true], + [4986,11,true,false,0,0,"key-86",0,"key-86","name 86",5986,true], + [4987,12,true,false,0,0,"key-87",0,"key-87","name 87",5987,true], + [4988,13,true,false,0,0,"key-88",0,"key-88","name 88",5988,true], + [4989,14,true,false,0,0,"key-89",0,"key-89","name 89",5989,true], + [4990,15,true,false,0,0,"key-90",0,"key-90","name 90",5990,true], + [4991,16,true,false,0,0,"key-91",0,"key-91","name 91",5991,true], + [4992,17,true,false,0,0,"key-92",0,"key-92","name 92",5992,true], + [4993,18,true,false,0,0,"key-93",0,"key-93","name 93",5993,true], + [4994,19,true,false,0,0,"key-94",0,"key-94","name 94",5994,true], [4995,20,true,false,0,0,"key-95",0,"key-95","name 95",5995,true], [4996,21,true,false,0,0,"key-96",0,"key-96","name 96",5996,true], [4997,22,true,false,0,0,"key-97",0,"key-97","name 97",5997,true], @@ -770,8 +770,8 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [10,1,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], - [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [10,0,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], + [11,1,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], ], }); @@ -793,9 +793,9 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [12,4,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [12,2,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], [13,3,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,2,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [14,4,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], ], }); @@ -818,9 +818,9 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [15,7,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], + [15,5,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], [16,6,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,5,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [17,7,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], ], }); @@ -911,8 +911,8 @@ describe("ServerProxy", () => { [7,7,true,false,0,0,"key-07",0,"key-07","name 07",1007,true], [8,8,true,false,0,0,"key-08",0,"key-08","name 08",1008,true], [9,9,true,false,0,0,"key-09",0,"key-09","name 09",1009,true], - [10,1,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], - [11,0,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], + [10,0,true,false,0,0,"key-10",0,"key-10","name 10",1010,true], + [11,1,true,false,0,0,"key-11",0,"key-11","name 11",1011,true], ], }); }); @@ -1023,16 +1023,16 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [12,0,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], - [13,1,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,2,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], - [15,3,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], - [16,4,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,5,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], - [18,6,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], - [19,7,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], - [20,9,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], - [21,8,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], + [12,2,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [13,3,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], + [14,4,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [15,5,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], + [16,6,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], + [17,7,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [18,8,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], + [19,9,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], + [20,0,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], + [21,1,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], ], }); }); @@ -1086,16 +1086,16 @@ describe("ServerProxy", () => { type: "viewport-update", clientViewportId: "client-vp-1", rows: [ - [12,9,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], - [13,8,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], - [14,7,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], - [15,6,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], - [16,5,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], - [17,4,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], - [18,3,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], - [19,2,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], - [20,1,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], - [21,0,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], + [12,0,true,false,0,0,"key-12",0,"key-12","name 12",1012,true], + [13,1,true,false,0,0,"key-13",0,"key-13","name 13",1013,true], + [14,2,true,false,0,0,"key-14",0,"key-14","name 14",1014,true], + [15,3,true,false,0,0,"key-15",0,"key-15","name 15",1015,true], + [16,4,true,false,0,0,"key-16",0,"key-16","name 16",1016,true], + [17,5,true,false,0,0,"key-17",0,"key-17","name 17",1017,true], + [18,6,true,false,0,0,"key-18",0,"key-18","name 18",1018,true], + [19,7,true,false,0,0,"key-19",0,"key-19","name 19",1019,true], + [20,8,true,false,0,0,"key-20",0,"key-20","name 20",1020,true], + [21,9,true,false,0,0,"key-21",0,"key-21","name 21",1021,true], [22,10,true,false,0,0,"key-22",0,"key-22","name 22",1022,true], ], }); diff --git a/vuu-ui/showcase/src/examples/utils/ArrayProxy.ts b/vuu-ui/packages/vuu-data-test/src/ArrayProxy.ts similarity index 100% rename from vuu-ui/showcase/src/examples/utils/ArrayProxy.ts rename to vuu-ui/packages/vuu-data-test/src/ArrayProxy.ts diff --git a/vuu-ui/packages/vuu-data-test/src/index.ts b/vuu-ui/packages/vuu-data-test/src/index.ts index 6efd6cf1b..02554a94f 100644 --- a/vuu-ui/packages/vuu-data-test/src/index.ts +++ b/vuu-ui/packages/vuu-data-test/src/index.ts @@ -1,3 +1,4 @@ +export * from "./ArrayProxy"; export * from "./schemas"; export * from "./TickingArrayDataSource"; export * from "./vuu-row-generator"; diff --git a/vuu-ui/packages/vuu-table/src/Table.css b/vuu-ui/packages/vuu-table/src/Table.css index ed478d25f..899af4e02 100644 --- a/vuu-ui/packages/vuu-table/src/Table.css +++ b/vuu-ui/packages/vuu-table/src/Table.css @@ -102,7 +102,8 @@ } .vuuTable-body { - height: var(--content-height) + height: var(--content-height); + position: relative; } .vuuPinLeft, .vuuPinRight { diff --git a/vuu-ui/packages/vuu-table/src/Table.tsx b/vuu-ui/packages/vuu-table/src/Table.tsx index 91d678083..04fe97ca7 100644 --- a/vuu-ui/packages/vuu-table/src/Table.tsx +++ b/vuu-ui/packages/vuu-table/src/Table.tsx @@ -36,6 +36,7 @@ import { TableHeader } from "./table-header/TableHeader"; import "./Table.css"; import type { DragDropState } from "@finos/vuu-ui-controls"; +import { ScrollingAPI } from "./useTableScroll"; const classBase = "vuuTable"; @@ -98,6 +99,11 @@ export interface TableProps onSelectionChange?: SelectionChangeHandler; renderBufferSize?: number; rowHeight?: number; + /** + * imperative API for scrolling table + */ + scrollingApiRef?: ForwardedRef; + /** * Selection Bookends style the left and right edge of a selection block. * They are optional, value defaults to zero. @@ -132,8 +138,9 @@ const TableCore = ({ onRowClick: onRowClickProp, onSelect, onSelectionChange, - renderBufferSize = 0, + renderBufferSize = 5, rowHeight = 20, + scrollingApiRef, selectionModel = "extended", showColumnHeaders = true, headerHeight = showColumnHeaders ? 25 : 0, @@ -148,6 +155,7 @@ const TableCore = ({ columns, data, draggableRow, + getRowOffset, handleContextMenuAction, headings, highlightedIndex, @@ -187,11 +195,12 @@ const TableCore = ({ onSelectionChange, renderBufferSize, rowHeight, + scrollingApiRef, selectionModel, size, }); - const className = cx(`${classBase}-contentContainer`, { + const contentContainerClassName = cx(`${classBase}-contentContainer`, { [`${classBase}-colLines`]: tableAttributes.columnSeparators, [`${classBase}-rowLines`]: tableAttributes.rowSeparators, // [`${classBase}-highlight`]: tableAttributes.showHighlightedRow, @@ -225,7 +234,7 @@ const TableCore = ({
@@ -258,9 +267,7 @@ const TableCore = ({ onClick={onRowClick} onDataEdited={onDataEdited} row={data} - offset={ - rowHeight * data[IDX] + viewportMeasurements.totalHeaderHeight - } + offset={getRowOffset(data)} onToggleGroup={onToggleGroup} zebraStripes={tableAttributes.zebraStripes} /> @@ -296,6 +303,7 @@ export const Table = forwardRef(function TableNext( onSelectionChange, renderBufferSize, rowHeight, + scrollingApiRef, selectionModel, showColumnHeaders, headerHeight, @@ -349,6 +357,7 @@ export const Table = forwardRef(function TableNext( onSelectionChange={onSelectionChange} renderBufferSize={renderBufferSize} rowHeight={rowHeight} + scrollingApiRef={scrollingApiRef} selectionModel={selectionModel} showColumnHeaders={showColumnHeaders} size={size} diff --git a/vuu-ui/packages/vuu-table/src/index.ts b/vuu-ui/packages/vuu-table/src/index.ts index 558444450..db5414018 100644 --- a/vuu-ui/packages/vuu-table/src/index.ts +++ b/vuu-ui/packages/vuu-table/src/index.ts @@ -7,4 +7,5 @@ export * from "./cell-renderers"; export type { RowProps } from "./Row"; export * from "./useControlledTableNavigation"; export * from "./useTableModel"; +export * from "./useTableScroll"; export * from "./useTableViewport"; diff --git a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts index da10cb990..94b547439 100644 --- a/vuu-ui/packages/vuu-table/src/table-dom-utils.ts +++ b/vuu-ui/packages/vuu-table/src/table-dom-utils.ts @@ -1,12 +1,15 @@ import { RefObject } from "react"; +/** + * [rowIndex, colIndex + */ export type CellPos = [number, number]; export const headerCellQuery = (colIdx: number) => `.vuuTable-col-headers .vuuTableHeaderCell:nth-child(${colIdx})`; export const dataCellQuery = (rowIdx: number, colIdx: number) => - `.vuuTable-body > [aria-rowindex='${rowIdx}'] > [role='cell']:nth-child(${ + `.vuuTable-body > [aria-rowindex='${rowIdx + 1}'] > [role='cell']:nth-child(${ colIdx + 1 })`; diff --git a/vuu-ui/packages/vuu-table/src/useDataSource.ts b/vuu-ui/packages/vuu-table/src/useDataSource.ts index d220b00a9..078ebb9e4 100644 --- a/vuu-ui/packages/vuu-table/src/useDataSource.ts +++ b/vuu-ui/packages/vuu-table/src/useDataSource.ts @@ -13,7 +13,6 @@ import { MovingWindow } from "./moving-window"; export interface DataSourceHookProps { dataSource: DataSource; - // onConfigChange?: (message: DataSourceConfigMessage) => void; onFeatureInvocation?: (message: VuuFeatureInvocationMessage) => void; onSizeChange: (size: number) => void; onSubscribed: (subscription: DataSourceSubscribedMessage) => void; @@ -38,7 +37,6 @@ export const useDataSource = ({ const data = useRef([]); const isMounted = useRef(true); const hasUpdated = useRef(false); - // const rafHandle = useRef(null); const rangeRef = useRef(NULL_RANGE); const dataWindow = useMemo( @@ -100,21 +98,6 @@ export const useDataSource = ({ }; }, [dataSource]); - // Keep until we'tre sure we don't need it for updates - // const refreshIfUpdated = useCallback(() => { - // if (isMounted.current) { - // console.log(`RAF updated data ? ${hasUpdated.current}`); - // if (hasUpdated.current) { - // forceUpdate({}); - // hasUpdated.current = false; - // } - // rafHandle.current = requestAnimationFrame(refreshIfUpdated); - // } - // }, [forceUpdate]); - // useEffect(() => { - // rafHandle.current = requestAnimationFrame(refreshIfUpdated); - // }, [refreshIfUpdated]); - useEffect(() => { if (dataSource.status === "disabled") { dataSource.enable?.(datasourceMessageHandler); @@ -129,6 +112,8 @@ export const useDataSource = ({ const setRange = useCallback( (range: VuuRange) => { + // TODO can we directly call setData here when we do an + // in-situ row scroll ? const fullRange = getFullRange(range, renderBufferSize); dataWindow.setRange(fullRange); dataSource.range = rangeRef.current = fullRange; diff --git a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts index 48861ec3d..b7d4acb03 100644 --- a/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts +++ b/vuu-ui/packages/vuu-table/src/useKeyboardNavigation.ts @@ -1,5 +1,6 @@ -import { useControlled } from "@salt-ds/core"; import { VuuRange } from "@finos/vuu-protocol-types"; +import { getIndexFromRowElement } from "@finos/vuu-utils"; +import { useControlled } from "@salt-ds/core"; import { KeyboardEvent, MouseEvent, @@ -8,7 +9,7 @@ import { useEffect, useRef, } from "react"; -import { ScrollDirection, ScrollRequestHandler } from "./useTableScroll"; +import { TableNavigationStyle } from "./Table"; import { CellPos, closestRowIndex, @@ -16,7 +17,7 @@ import { getTableCell, headerCellQuery, } from "./table-dom-utils"; -import { TableNavigationStyle } from "./Table"; +import { ScrollDirection, ScrollRequestHandler } from "./useTableScroll"; const rowNavigationKeys = new Set([ "Home", @@ -215,9 +216,9 @@ NavigationHookProps) => { const colIdx = parseInt(tableCell.dataset.idx ?? "-1", 10); return [-1, colIdx]; } else { - const focusedRow = tableCell.closest("[role='row']"); + const focusedRow = tableCell.closest("[role='row']") as HTMLElement; if (focusedRow) { - const rowIdx = parseInt(focusedRow.ariaRowIndex ?? "-1", 10); + const rowIdx = getIndexFromRowElement(focusedRow); // TODO will get trickier when we introduce horizontal virtualisation const colIdx = Array.from(focusedRow.childNodes).indexOf(tableCell); return [rowIdx, colIdx]; @@ -236,11 +237,9 @@ NavigationHookProps) => { focusableCell.current = activeCell; activeCell.setAttribute("tabindex", "0"); } - const [direction, distance] = howFarIsCellOutsideViewport(activeCell); - if (direction && distance) { - requestScroll?.({ type: "scroll-distance", distance, direction }); - } - console.log(`activeCell focus`); + // TODO needs to be scroll cell + console.log(`scroll row ${cellPos[0]}`); + requestScroll?.({ type: "scroll-row", rowIndex: cellPos[0] }); activeCell.focus({ preventScroll: true }); } } @@ -336,18 +335,9 @@ NavigationHookProps) => { const scrollRowIntoViewIfNecessary = useCallback( (rowIndex: number) => { - const { current: container } = containerRef; - const activeRow = container?.querySelector( - `[aria-rowindex="${rowIndex}"]` - ) as HTMLElement; - if (activeRow) { - const [direction, distance] = howFarIsRowOutsideViewport(activeRow); - if (direction && distance) { - requestScroll?.({ type: "scroll-distance", distance, direction }); - } - } + requestScroll?.({ type: "scroll-row", rowIndex }); }, - [containerRef, requestScroll] + [requestScroll] ); const moveHighlightedRow = useCallback( @@ -358,6 +348,7 @@ NavigationHookProps) => { : nextCellPos(key, [highlighted ?? -1, 0], columnCount, rowCount); if (nextRowIdx !== highlighted) { setHighlightedIndex(nextRowIdx); + // TO(DO make this a scroll request) scrollRowIntoViewIfNecessary(nextRowIdx); } }, diff --git a/vuu-ui/packages/vuu-table/src/useTable.ts b/vuu-ui/packages/vuu-table/src/useTable.ts index 061cce244..203788bb3 100644 --- a/vuu-ui/packages/vuu-table/src/useTable.ts +++ b/vuu-ui/packages/vuu-table/src/useTable.ts @@ -61,7 +61,6 @@ import { } from "./useTableModel"; import { useTableScroll } from "./useTableScroll"; import { useTableViewport } from "./useTableViewport"; -import { useVirtualViewport } from "./useVirtualViewport"; import { useTableAndColumnSettings } from "./useTableAndColumnSettings"; const stripInternalProperties = (tableConfig: TableConfig): TableConfig => { @@ -90,6 +89,7 @@ export interface TableHookProps | "onSelectionChange" | "onRowClick" | "renderBufferSize" + | "scrollingApiRef" > { containerRef: RefObject; headerHeight: number; @@ -136,6 +136,7 @@ export const useTable = ({ onSelectionChange, renderBufferSize = 0, rowHeight = 20, + scrollingApiRef, selectionModel, size, }: TableHookProps) => { @@ -227,7 +228,10 @@ export const useTable = ({ const initialRange = useInitialValue({ from: 0, - to: viewportMeasurements.rowCount, + to: + viewportMeasurements.rowCount === 0 + ? 0 + : viewportMeasurements.rowCount + 1, }); const onSubscribed = useCallback( @@ -466,26 +470,20 @@ export const useTable = ({ [columns, dataSource, dispatchColumnAction] ); - const { onVerticalScroll } = useVirtualViewport({ - columns, - getRowAtPosition, - setRange, - viewportMeasurements, - }); - const handleVerticalScroll = useCallback( - (scrollTop: number) => { - onVerticalScroll(scrollTop); + (_: number, pctScrollTop: number) => { + setPctScrollTop(pctScrollTop); }, - [onVerticalScroll] + [setPctScrollTop] ); const { requestScroll, ...scrollProps } = useTableScroll({ - maxScrollLeft: viewportMeasurements.maxScrollContainerScrollHorizontal, - maxScrollTop: viewportMeasurements.maxScrollContainerScrollVertical, + getRowAtPosition, rowHeight, + scrollingApiRef, + setRange, onVerticalScroll: handleVerticalScroll, - viewportRowCount: viewportMeasurements.rowCount, + viewportMeasurements, }); const { @@ -680,6 +678,7 @@ export const useTable = ({ columnMap, columns, data, + getRowOffset, handleContextMenuAction, headings, highlightedIndex: highlightedIndexRef.current, diff --git a/vuu-ui/packages/vuu-table/src/useTableScroll.ts b/vuu-ui/packages/vuu-table/src/useTableScroll.ts index ef47a7469..180a6fc10 100644 --- a/vuu-ui/packages/vuu-table/src/useTableScroll.ts +++ b/vuu-ui/packages/vuu-table/src/useTableScroll.ts @@ -1,4 +1,14 @@ -import { useCallback, useRef } from "react"; +import { getRowElementAtIndex, RowAtPositionFunc } from "@finos/vuu-utils"; +import { VuuRange } from "@finos/vuu-protocol-types"; +import { + ForwardedRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from "react"; +import { ViewportMeasurements } from "./useTableViewport"; export type ScrollDirectionVertical = "up" | "down"; export type ScrollDirectionHorizontal = "left" | "right"; @@ -6,6 +16,13 @@ export type ScrollDirection = | ScrollDirectionVertical | ScrollDirectionHorizontal; +/** + * scroll into view the row at given index posiiton. + */ +export interface ScrollRequestRow { + rowIndex: number; + type: "scroll-row"; +} export interface ScrollRequestEnd { type: "scroll-end"; direction: "home" | "end"; @@ -16,19 +33,18 @@ export interface ScrollRequestPage { direction: ScrollDirectionVertical; } -export interface ScrollRequestDistance { - direction: ScrollDirection; - type: "scroll-distance"; - distance: number; -} - export type ScrollRequest = | ScrollRequestPage - | ScrollRequestDistance - | ScrollRequestEnd; + | ScrollRequestEnd + | ScrollRequestRow; export type ScrollRequestHandler = (request: ScrollRequest) => void; +export interface ScrollingAPI { + scrollToIndex: (itemIndex: number) => void; + scrollToKey: (rowKey: string) => void; +} + const getPctScroll = (container: HTMLElement) => { const { scrollLeft, scrollTop } = container; const { clientHeight, clientWidth, scrollHeight, scrollWidth } = container; @@ -37,12 +53,46 @@ const getPctScroll = (container: HTMLElement) => { return [pctScrollLeft, pctScrollTop]; }; +export const noScrolling: ScrollingAPI = { + scrollToIndex: () => undefined, + scrollToKey: () => undefined, +}; + interface CallbackRefHookProps { onAttach?: (el: T) => void; onDetach: (el: T) => void; label?: string; } +const NO_SCROLL_NECESSARY = [undefined, undefined] as const; + +export const howFarIsRowOutsideViewport = ( + rowEl: HTMLElement, + totalHeaderHeight: number, + contentContainer = rowEl.closest(".vuuTable-contentContainer") +): readonly [ScrollDirection | undefined, number | undefined] => { + //TODO lots of scope for optimisation here + if (contentContainer) { + // TODO take totalHeaderHeight into consideration + const viewport = contentContainer?.getBoundingClientRect(); + const upperBoundary = viewport.top + totalHeaderHeight; + const row = rowEl.getBoundingClientRect(); + if (row) { + if (row.bottom > viewport.bottom) { + return ["down", row.bottom - viewport.bottom]; + } else if (row.top < upperBoundary) { + return ["up", row.top - upperBoundary]; + } else { + return NO_SCROLL_NECESSARY; + } + } else { + throw Error("Whats going on, row not found"); + } + } else { + throw Error("Whats going on, scrollbar container not found"); + } +}; + const useCallbackRef = ({ onAttach, onDetach, @@ -65,27 +115,50 @@ const useCallbackRef = ({ }; export interface TableScrollHookProps { - maxScrollLeft: number; - maxScrollTop: number; + getRowAtPosition: RowAtPositionFunc; onHorizontalScroll?: (scrollLeft: number) => void; onVerticalScroll?: (scrollTop: number, pctScrollTop: number) => void; rowHeight: number; - viewportRowCount: number; + scrollingApiRef?: ForwardedRef; + setRange: (range: VuuRange) => void; + viewportMeasurements: ViewportMeasurements; } export const useTableScroll = ({ - maxScrollLeft, - maxScrollTop, + getRowAtPosition, onHorizontalScroll, onVerticalScroll, - rowHeight, - viewportRowCount, + scrollingApiRef, + setRange, + viewportMeasurements, }: TableScrollHookProps) => { + const firstRowRef = useRef(0); const contentContainerScrolledRef = useRef(false); const scrollPosRef = useRef({ scrollTop: 0, scrollLeft: 0 }); const scrollbarContainerRef = useRef(null); const contentContainerRef = useRef(null); + const { + appliedPageSize, + isVirtualScroll, + maxScrollContainerScrollHorizontal: maxScrollLeft, + maxScrollContainerScrollVertical: maxScrollTop, + rowCount: viewportRowCount, + totalHeaderHeight, + } = viewportMeasurements; + + const handleVerticalScroll = useCallback( + (scrollTop: number, pctScrollTop: number) => { + onVerticalScroll?.(scrollTop, pctScrollTop); + const firstRow = getRowAtPosition(scrollTop); + if (firstRow !== firstRowRef.current) { + firstRowRef.current = firstRow; + setRange({ from: firstRow, to: firstRow + viewportRowCount + 1 }); + } + }, + [getRowAtPosition, onVerticalScroll, setRange, viewportRowCount] + ); + const handleScrollbarContainerScroll = useCallback(() => { const { current: contentContainer } = contentContainerRef; const { current: scrollbarContainer } = scrollbarContainerRef; @@ -95,7 +168,7 @@ export const useTableScroll = ({ } else if (contentContainer && scrollbarContainer) { const [pctScrollLeft, pctScrollTop] = getPctScroll(scrollbarContainer); const rootScrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - const rootScrollTop = Math.round(pctScrollTop * maxScrollTop); + const rootScrollTop = pctScrollTop * maxScrollTop; contentContainer.scrollTo({ left: rootScrollLeft, top: rootScrollTop, @@ -113,20 +186,19 @@ export const useTableScroll = ({ const { scrollLeft, scrollTop } = contentContainer; const [pctScrollLeft, pctScrollTop] = getPctScroll(contentContainer); contentContainerScrolledRef.current = true; - scrollbarContainer.scrollLeft = Math.round(pctScrollLeft * maxScrollLeft); - scrollbarContainer.scrollTop = Math.round(pctScrollTop * maxScrollTop); + scrollbarContainer.scrollTop = pctScrollTop * maxScrollTop; if (scrollPos.scrollTop !== scrollTop) { scrollPos.scrollTop = scrollTop; - onVerticalScroll?.(scrollTop, pctScrollTop); + handleVerticalScroll(scrollTop, pctScrollTop); } if (scrollPos.scrollLeft !== scrollLeft) { scrollPos.scrollLeft = scrollLeft; onHorizontalScroll?.(scrollLeft); } } - }, [maxScrollLeft, maxScrollTop, onHorizontalScroll, onVerticalScroll]); + }, [handleVerticalScroll, maxScrollLeft, maxScrollTop, onHorizontalScroll]); const handleAttachScrollbarContainer = useCallback( (el: HTMLDivElement) => { @@ -174,48 +246,70 @@ export const useTableScroll = ({ onDetach: handleDetachScrollbarContainer, }); - //TODO should this be async ? const requestScroll: ScrollRequestHandler = useCallback( (scrollRequest) => { const { current: scrollbarContainer } = contentContainerRef; if (scrollbarContainer) { const { scrollLeft, scrollTop } = scrollbarContainer; contentContainerScrolledRef.current = false; - if (scrollRequest.type === "scroll-distance") { - let newScrollLeft = scrollLeft; - let newScrollTop = scrollTop; - if ( - scrollRequest.direction === "up" || - scrollRequest.direction === "down" - ) { - newScrollTop = Math.min( - Math.max(0, scrollTop + scrollRequest.distance), - maxScrollTop - ); - } else { - newScrollLeft = Math.min( - Math.max(0, scrollLeft + scrollRequest.distance), - maxScrollLeft + if (scrollRequest.type === "scroll-row") { + const activeRow = getRowElementAtIndex( + scrollbarContainer, + scrollRequest.rowIndex + ); + if (activeRow !== null) { + const [direction, distance] = howFarIsRowOutsideViewport( + activeRow, + totalHeaderHeight ); + if (direction && distance) { + if (isVirtualScroll) { + console.log( + `virtual scroll row required ${direction} ${distance} + first Row ${firstRowRef.current}` + ); + // const from = firstRowRef.current + 1; + // console.log(`setRange from ${from}`); + // setRange({ from, to: from + viewportRowCount + 1 }); + } else { + let newScrollLeft = scrollLeft; + let newScrollTop = scrollTop; + if (direction === "up" || direction === "down") { + newScrollTop = Math.min( + Math.max(0, scrollTop + distance), + maxScrollTop + ); + } else { + newScrollLeft = Math.min( + Math.max(0, scrollLeft + distance), + maxScrollLeft + ); + } + scrollbarContainer.scrollTo({ + top: newScrollTop, + left: newScrollLeft, + behavior: "smooth", + }); + } + } } - scrollbarContainer.scrollTo({ - top: newScrollTop, - left: newScrollLeft, - behavior: "smooth", - }); } else if (scrollRequest.type === "scroll-page") { const { direction } = scrollRequest; - const scrollBy = - viewportRowCount * (direction === "down" ? rowHeight : -rowHeight); - const newScrollTop = Math.min( - Math.max(0, scrollTop + scrollBy), - maxScrollTop - ); - scrollbarContainer.scrollTo({ - top: newScrollTop, - left: scrollLeft, - behavior: "auto", - }); + if (isVirtualScroll) { + console.log(`need a virtual page scroll`); + } else { + const scrollBy = + direction === "down" ? appliedPageSize : -appliedPageSize; + const newScrollTop = Math.min( + Math.max(0, scrollTop + scrollBy), + maxScrollTop + ); + scrollbarContainer.scrollTo({ + top: newScrollTop, + left: scrollLeft, + behavior: "auto", + }); + } } else if (scrollRequest.type === "scroll-end") { const { direction } = scrollRequest; const scrollTo = direction === "end" ? maxScrollTop : 0; @@ -227,9 +321,50 @@ export const useTableScroll = ({ } } }, - [maxScrollLeft, maxScrollTop, rowHeight, viewportRowCount] + [ + appliedPageSize, + isVirtualScroll, + maxScrollLeft, + maxScrollTop, + setRange, + totalHeaderHeight, + viewportRowCount, + ] + ); + + const scrollHandles: ScrollingAPI = useMemo( + () => ({ + scrollToIndex: (rowIndex: number) => { + if (scrollbarContainerRef.current) { + const scrollPos = (rowIndex - 30) * 20; + scrollbarContainerRef.current.scrollTop = scrollPos; + } + }, + scrollToKey: (rowKey: string) => { + console.log(`scrollToKey ${rowKey}`); + }, + }), + [] + ); + + useImperativeHandle( + scrollingApiRef, + () => { + if (scrollbarContainerRef.current) { + return scrollHandles; + } else { + return noScrolling; + } + }, + [scrollHandles] ); + useEffect(() => { + const { current: from } = firstRowRef; + const rowRange = { from, to: from + viewportRowCount + 1 }; + setRange(rowRange); + }, [setRange, viewportRowCount]); + return { /** Ref to be assigned to ScrollbarContainer */ scrollbarContainerRef: scrollbarContainerCallbackRef, diff --git a/vuu-ui/packages/vuu-table/src/useTableViewport.ts b/vuu-ui/packages/vuu-table/src/useTableViewport.ts index 18f7e6f2d..2c8745b8e 100644 --- a/vuu-ui/packages/vuu-table/src/useTableViewport.ts +++ b/vuu-ui/packages/vuu-table/src/useTableViewport.ts @@ -24,8 +24,10 @@ export interface TableViewportHookProps { } export interface ViewportMeasurements { + appliedPageSize: number; contentHeight: number; horizontalScrollbarHeight: number; + isVirtualScroll: boolean; maxScrollContainerScrollHorizontal: number; maxScrollContainerScrollVertical: number; pinnedWidthLeft: number; @@ -44,14 +46,17 @@ export interface TableViewportHookResult extends ViewportMeasurements { } // Too simplistic, it depends on rowHeight -const MAX_RAW_ROWS = 1_500_000; +// const MAX_RAW_ROWS = 1_000_000; +const MAX_RAW_ROWS = 100_000; const UNMEASURED_VIEWPORT: TableViewportHookResult = { + appliedPageSize: 0, contentHeight: 0, contentWidth: 0, getRowAtPosition: () => -1, getRowOffset: () => -1, horizontalScrollbarHeight: 0, + isVirtualScroll: false, maxScrollContainerScrollHorizontal: 0, maxScrollContainerScrollVertical: 0, pinnedWidthLeft: 0, @@ -94,32 +99,33 @@ export const useTableViewport = ({ size, }: TableViewportHookProps): TableViewportHookResult => { const pctScrollTopRef = useRef(0); - const appliedRowCount = Math.min(rowCount, MAX_RAW_ROWS); - const appliedContentHeight = appliedRowCount * rowHeight; + // TODO we are limited by pixels not an arbitraty number of rows + const pixelContentHeight = rowHeight * Math.min(rowCount, MAX_RAW_ROWS); const virtualContentHeight = rowCount * rowHeight; - const virtualisedExtent = virtualContentHeight - appliedContentHeight; + const virtualisedExtent = virtualContentHeight - pixelContentHeight; const { pinnedWidthLeft, pinnedWidthRight, unpinnedWidth } = useMemo( () => measurePinnedColumns(columns), [columns] ); - const [actualRowOffset, actualRowAtPosition] = useMemo( - () => actualRowPositioning(rowHeight), - [rowHeight] - ); + const totalHeaderHeightRef = useRef(headerHeight); + useMemo(() => { + totalHeaderHeightRef.current = headerHeight * (1 + headings.length); + }, [headerHeight, headings.length]); - const [getRowOffset, getRowAtPosition] = useMemo(() => { - if (virtualisedExtent) { - return virtualRowPositioning( - rowHeight, - virtualisedExtent, - pctScrollTopRef - ); - } else { - return [actualRowOffset, actualRowAtPosition]; - } - }, [actualRowAtPosition, actualRowOffset, virtualisedExtent, rowHeight]); + const [getRowOffset, getRowAtPosition, isVirtualScroll] = + useMemo(() => { + if (virtualisedExtent) { + return virtualRowPositioning( + rowHeight, + virtualisedExtent, + pctScrollTopRef + ); + } else { + return actualRowPositioning(rowHeight); + } + }, [virtualisedExtent, rowHeight]); const setPctScrollTop = useCallback((scrollPct: number) => { pctScrollTopRef.current = scrollPct; @@ -127,37 +133,42 @@ export const useTableViewport = ({ return useMemo(() => { if (size) { - const headingsDepth = headings.length; + const { current: totalHeaderHeight } = totalHeaderHeightRef; + // TODO determine this at runtime const scrollbarSize = 15; const contentWidth = pinnedWidthLeft + unpinnedWidth + pinnedWidthRight; const horizontalScrollbarHeight = contentWidth > size.width ? scrollbarSize : 0; - const totalHeaderHeight = headerHeight * (1 + headingsDepth); const maxScrollContainerScrollVertical = - appliedContentHeight - + pixelContentHeight - ((size?.height ?? 0) - horizontalScrollbarHeight) + totalHeaderHeight; const maxScrollContainerScrollHorizontal = contentWidth - size.width + pinnedWidthLeft; const visibleRows = (size.height - headerHeight) / rowHeight; const count = Number.isInteger(visibleRows) - ? visibleRows + 1 + ? visibleRows : Math.ceil(visibleRows); const viewportBodyHeight = size.height - totalHeaderHeight; const verticalScrollbarWidth = - appliedContentHeight > viewportBodyHeight ? scrollbarSize : 0; + pixelContentHeight > viewportBodyHeight ? scrollbarSize : 0; + + const appliedPageSize = + count * rowHeight * (pixelContentHeight / virtualContentHeight); return { - contentHeight: appliedContentHeight, + appliedPageSize, + contentHeight: pixelContentHeight, + contentWidth, getRowAtPosition, getRowOffset, + isVirtualScroll, horizontalScrollbarHeight, maxScrollContainerScrollHorizontal, maxScrollContainerScrollVertical, pinnedWidthLeft, pinnedWidthRight, rowCount: count, - contentWidth, setPctScrollTop, totalHeaderHeight, verticalScrollbarWidth, @@ -167,16 +178,17 @@ export const useTableViewport = ({ return UNMEASURED_VIEWPORT; } }, [ - size, - headings.length, + getRowAtPosition, + getRowOffset, + headerHeight, + isVirtualScroll, pinnedWidthLeft, unpinnedWidth, pinnedWidthRight, - appliedContentHeight, - headerHeight, + pixelContentHeight, rowHeight, - getRowAtPosition, - getRowOffset, setPctScrollTop, + size, + virtualContentHeight, ]); }; diff --git a/vuu-ui/packages/vuu-table/src/useVirtualViewport.ts b/vuu-ui/packages/vuu-table/src/useVirtualViewport.ts deleted file mode 100644 index b95486ccf..000000000 --- a/vuu-ui/packages/vuu-table/src/useVirtualViewport.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { RuntimeColumnDescriptor } from "@finos/vuu-table-types"; -import { VuuRange } from "@finos/vuu-protocol-types"; -import { RowAtPositionFunc } from "@finos/vuu-utils"; -import { useCallback, useEffect, useRef } from "react"; -import { ViewportMeasurements } from "./useTableViewport"; - -export interface VirtualViewportHookProps { - columns: RuntimeColumnDescriptor[]; - getRowAtPosition: RowAtPositionFunc; - setRange: (range: VuuRange) => void; - viewportMeasurements: ViewportMeasurements; -} - -export const useVirtualViewport = ({ - getRowAtPosition, - setRange, - viewportMeasurements, -}: VirtualViewportHookProps) => { - const firstRowRef = useRef(0); - const { rowCount: viewportRowCount } = viewportMeasurements; - - const handleVerticalScroll = useCallback( - (scrollTop: number) => { - const firstRow = getRowAtPosition(scrollTop); - if (firstRow !== firstRowRef.current) { - firstRowRef.current = firstRow; - setRange({ from: firstRow, to: firstRow + viewportRowCount }); - } - }, - [getRowAtPosition, setRange, viewportRowCount] - ); - - useEffect(() => { - const { current: from } = firstRowRef; - const rowRange = { from, to: from + viewportRowCount }; - setRange(rowRange); - }, [setRange, viewportRowCount]); - - return { - onVerticalScroll: handleVerticalScroll, - }; -}; diff --git a/vuu-ui/packages/vuu-utils/src/keyset.ts b/vuu-ui/packages/vuu-utils/src/keyset.ts index e503c202b..883ba358d 100644 --- a/vuu-ui/packages/vuu-utils/src/keyset.ts +++ b/vuu-ui/packages/vuu-utils/src/keyset.ts @@ -14,7 +14,7 @@ export class KeySet { public next(): number { if (this.free.length > 0) { - return this.free.pop() as number; + return this.free.shift() as number; } else { return this.nextKeyValue++; } @@ -59,7 +59,7 @@ export class KeySet { public toDebugString() { return Array.from(this.keys.entries()) - .map((k, v) => `${k}=>${v}`) + .map(([k, v]) => `${k}=>${v}`) .join(","); } } diff --git a/vuu-ui/packages/vuu-utils/src/row-utils.ts b/vuu-ui/packages/vuu-utils/src/row-utils.ts index 698f50d0f..9196e2424 100644 --- a/vuu-ui/packages/vuu-utils/src/row-utils.ts +++ b/vuu-ui/packages/vuu-utils/src/row-utils.ts @@ -1,5 +1,6 @@ -import { DataSourceRow } from "@finos/vuu-data-types"; -import { MutableRefObject } from "react"; +//TODO this all probably belongs in vuu-table +import type { DataSourceRow } from "@finos/vuu-data-types"; +import type { MutableRefObject } from "react"; import { metadataKeys } from "./column-utils"; const { IDX } = metadataKeys; @@ -10,29 +11,68 @@ export type RowOffsetFunc = ( ) => number; export type RowAtPositionFunc = (position: number) => number; -export type RowPositioning = [RowOffsetFunc, RowAtPositionFunc]; +/** + * RowOffset function, RowAtPosition function, isVirtualScroll + */ +export type RowPositioning = [RowOffsetFunc, RowAtPositionFunc, boolean]; export const actualRowPositioning = (rowHeight: number): RowPositioning => [ (row) => row[IDX] * rowHeight, (position) => Math.floor(position / rowHeight), + false, ]; +/** + * return functions for determining a) the pixel offset to apply to a row, given the + * row index and b) the index of the row at a given scroll offset. This implementation + * is used when we are forced to 'virtualise' scrolling - because the number of rows + * is high enough that we cannot create a large enough HTML content container. + * + * @param rowHeight + * @param virtualisedExtent + * @param pctScrollTop + * @returns + */ export const virtualRowPositioning = ( rowHeight: number, - additionalPixelsNeeded: number, + virtualisedExtent: number, pctScrollTop: MutableRefObject ): RowPositioning => [ (row) => { - const rowOffset = pctScrollTop.current * additionalPixelsNeeded; + const rowOffset = pctScrollTop.current * virtualisedExtent; return row[IDX] * rowHeight - rowOffset; }, + /* + Return index position of closest row + */ (position) => { - const rowOffset = pctScrollTop.current * additionalPixelsNeeded; - const result = Math.floor((position + rowOffset) / rowHeight); - return result; + const rowOffset = pctScrollTop.current * virtualisedExtent; + return Math.round((position + rowOffset) / rowHeight); }, + true, ]; +export const getRowElementAtIndex = ( + container: HTMLDivElement, + rowIndex: number +) => { + if (rowIndex === -1) { + return null; + } else { + const activeRow = container.querySelector( + `[aria-rowindex="${rowIndex + 1}"]` + ) as HTMLElement; + + if (activeRow) { + return activeRow; + } else { + throw Error( + `getRowElementAtIndex no row found for index index ${rowIndex}` + ); + } + } +}; + export const getIndexFromRowElement = (rowElement: HTMLElement) => { const rowIndex = rowElement.ariaRowIndex; if (rowIndex != null) { diff --git a/vuu-ui/packages/vuu-utils/test/keyset.test.js b/vuu-ui/packages/vuu-utils/test/keyset.test.js index d09c94c82..fd25f6e99 100644 --- a/vuu-ui/packages/vuu-utils/test/keyset.test.js +++ b/vuu-ui/packages/vuu-utils/test/keyset.test.js @@ -49,7 +49,7 @@ describe("KeySet", () => { keySet.reset({ from: 2, to: 12 }); expect(keySet.keys.size).toEqual(10); expect([...keySet.keys.keys()]).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect([...keySet.keys.values()]).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 1, 0]); + expect([...keySet.keys.values()]).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 0, 1]); }); it("re-initialises a keyset, forwards, no overlap", () => { @@ -59,7 +59,7 @@ describe("KeySet", () => { expect([...keySet.keys.keys()]).toEqual([ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ]); - expect([...keySet.keys.values()]).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); it("re-initialises a keyset, backwards, with overlap", () => { @@ -69,7 +69,7 @@ describe("KeySet", () => { expect([...keySet.keys.keys()]).toEqual([ 10, 11, 12, 13, 14, 15, 16, 17, 8, 9, ]); - expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 9, 8]); + expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); it("re-initialises a keyset, backwards, no overlap", () => { @@ -77,7 +77,7 @@ describe("KeySet", () => { keySet.reset({ from: 0, to: 10 }); expect(keySet.keys.size).toEqual(10); expect([...keySet.keys.keys()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); - expect([...keySet.keys.values()]).toEqual([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + expect([...keySet.keys.values()]).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); }); }); diff --git a/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx b/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx new file mode 100644 index 000000000..471bcb816 --- /dev/null +++ b/vuu-ui/showcase/src/examples/Table/BigData.examples.tsx @@ -0,0 +1,143 @@ +import { ArrayDataSource } from "@finos/vuu-data-local"; +import { ArrayProxy, RowAtIndexFunc } from "@finos/vuu-data-test"; +import { DataSource } from "@finos/vuu-data-types"; +import { Toolbar } from "@finos/vuu-layout/src"; +import { VuuRowDataItemType } from "@finos/vuu-protocol-types"; +import { noScrolling, Table } from "@finos/vuu-table"; +import { ColumnDescriptor, TableConfig } from "@finos/vuu-table-types"; +import { Button, Input } from "@salt-ds/core"; +import { ScrollingAPI } from "@finos/vuu-table"; +import { + ChangeEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from "react"; + +let displaySequence = 1; + +type RowGenerator = ( + columns: string[] +) => RowAtIndexFunc; + +export type ColumnGenerator = (count: number) => ColumnDescriptor[]; + +const columnGenerator: ColumnGenerator = (count) => { + return [{ name: "row number", width: 150 }].concat( + Array(count) + .fill(true) + .map((_, i) => { + const name = `column ${i + 1}`; + return { name, width: 150 }; + }) + ); +}; + +const rowGenerator: RowGenerator = (columns: string[]) => (index) => { + return [`row ${index + 1}`].concat( + Array(columns.length) + .fill(true) + .map((v, j) => `value ${j + 1} @ ${index + 1}`) + ); +}; + +export const SimpleTable = () => { + const config = useMemo( + () => ({ + columns: columnGenerator(5), + }), + [] + ); + + const dataSource = useMemo(() => { + const data = new ArrayProxy( + 1_000_000_000, + rowGenerator(config.columns.map((col) => col.name)) + ); + return new ArrayDataSource({ + columnDescriptors: config.columns, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + data, + }); + }, [config.columns]); + + return ( + + ); +}; +SimpleTable.displaySequence = displaySequence++; + +export const TableScrollingAPI = () => { + const [rowInputValue, setRowInputValue] = useState(""); + const [scrollPosition, setScrollPosition] = useState(""); + const scrollingAPI = useRef(noScrolling); + + const handleChangeRowInput = useCallback< + ChangeEventHandler + >((evt) => { + const { value } = evt.target as HTMLInputElement; + setRowInputValue(value); + }, []); + const handleChangeScrollPosition = useCallback< + ChangeEventHandler + >((evt) => { + const { value } = evt.target as HTMLInputElement; + setScrollPosition(value); + }, []); + + const handleScrollToIndex = useCallback(() => { + const rowIndex = parseInt(rowInputValue); + if (!isNaN(rowIndex)) { + scrollingAPI.current.scrollToIndex(rowIndex); + } + }, [rowInputValue]); + + const handleScrollToPosition = useCallback(() => { + const rowIndex = parseInt(rowInputValue); + if (!isNaN(rowIndex)) { + scrollingAPI.current.scrollToIndex(rowIndex); + } + }, [rowInputValue]); + + const config = useMemo( + () => ({ + columns: columnGenerator(5), + zebraStripes: true, + }), + [] + ); + + const dataSource = useMemo(() => { + const data = new ArrayProxy( + 1_000_000_000, + rowGenerator(config.columns.map((col) => col.name)) + ); + return new ArrayDataSource({ + columnDescriptors: config.columns, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + data, + }); + }, [config.columns]); + + return ( + <> +
+ + + + + + + + ); +}; +TableScrollingAPI.displaySequence = displaySequence++; diff --git a/vuu-ui/showcase/src/examples/Table/index.ts b/vuu-ui/showcase/src/examples/Table/index.ts index 55ccc428b..b936a857e 100644 --- a/vuu-ui/showcase/src/examples/Table/index.ts +++ b/vuu-ui/showcase/src/examples/Table/index.ts @@ -2,4 +2,5 @@ export * as TableList from "./TableList.examples"; export * as Table from "./Table.examples"; export * as BASKET from "./BASKET.examples"; export * as SIMUL from "./SIMUL.examples"; +export * as BigData from "./BigData.examples"; export * as TableLayout from "./TableLayout.examples"; diff --git a/vuu-ui/showcase/src/examples/html/components/BigScrollable.ts b/vuu-ui/showcase/src/examples/html/components/BigScrollable.ts new file mode 100644 index 000000000..e69de29bb diff --git a/vuu-ui/showcase/src/examples/utils/ArrayLike.ts b/vuu-ui/showcase/src/examples/utils/ArrayLike.ts deleted file mode 100644 index 950d057c7..000000000 --- a/vuu-ui/showcase/src/examples/utils/ArrayLike.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { WindowRange } from "@finos/vuu-utils"; - -export class ArrayLike { - public range: WindowRange; - public data: T[]; - public length = 0; - - constructor(input: T[], size: number, range: WindowRange) { - this.range = range; - this.data = input; - this.length = size; - - const handler = { - get: (target: ArrayLike, prop: string | symbol): any => { - if (prop === "length") { - return target.length; - } - if (prop === "slice") { - return target.slice; - } - if (prop === "toString") { - return target.debug; - } - if (typeof prop === "string") { - const index = parseInt(prop, 10); - if (!isNaN(index)) { - return target.getItem(index); - } - } - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return target.data[prop]; - }, - set: (target: ArrayLike, prop: string, newVal: any) => { - if (prop === "length") { - target.length = newVal; - return true; - } - if (prop === "data") { - target.data = newVal; - return true; - } - if (prop === "range") { - target.range = newVal; - return true; - } - throw Error(`ArrayLike is immutable except for length`); - }, - }; - return new Proxy(this, handler); - } - - getItem = (index: number) => { - const offset = this.range.from; - return this.data[index - offset]; - }; - - slice = (from: number, to: number) => { - const offset = this.range.from; - const out = []; - for (let i = from; i < to; i++) { - const index = i - offset; - if (this.data[index] !== undefined) { - out.push(this.data[index]); - } else { - out.push({ label: "???", value: "" }); - } - } - return out; - }; - - debug = () => { - return `ArrayLike: range ${this.range.from} - ${this.range.to} data - ${JSON.stringify(this.data[0])} - ${JSON.stringify( - this.data[this.data.length - 1] - )}`; - }; -} diff --git a/vuu-ui/showcase/src/examples/utils/index.ts b/vuu-ui/showcase/src/examples/utils/index.ts index a71de83f3..c98847882 100644 --- a/vuu-ui/showcase/src/examples/utils/index.ts +++ b/vuu-ui/showcase/src/examples/utils/index.ts @@ -1,5 +1,3 @@ -export * from "./ArrayLike"; -export * from "./ArrayProxy"; export * from "./ErrorDisplay"; export * from "./useAutoLoginToVuuServer"; export * from "./useTestDataSource";