diff --git a/vuu-ui/packages/vuu-data/src/message-utils.ts b/vuu-ui/packages/vuu-data/src/message-utils.ts index af1de3e6b..029250135 100644 --- a/vuu-ui/packages/vuu-data/src/message-utils.ts +++ b/vuu-ui/packages/vuu-data/src/message-utils.ts @@ -43,3 +43,14 @@ export const getFirstAndLastRows = ( const lastRow = rows.at(-1) as VuuRow; return [firstRow, lastRow]; }; + +export type ViewportRowMap = { [key: string]: VuuRow[] }; +export const groupRowsByViewport = (rows: VuuRow[]): ViewportRowMap => { + const result: ViewportRowMap = {}; + for (const row of rows) { + const rowsForViewport = + result[row.viewPortId] || (result[row.viewPortId] = []); + rowsForViewport.push(row); + } + return result; +}; diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts index 52df03bdf..3e5a8d124 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/server-proxy.ts @@ -20,6 +20,7 @@ import { DataSourceVisualLinkRemovedMessage, } from "../data-source"; import { + groupRowsByViewport, isVuuMenuRpcRequest, stripRequestId, WithRequestId, @@ -107,18 +108,6 @@ function addLabelsToLinks( }); } -const byViewportRowIdxTimestamp = (row1: VuuRow, row2: VuuRow) => { - if (row1.viewPortId === row2.viewPortId) { - if (row1.rowIndex === row2.rowIndex) { - return row1.ts > row2.ts ? 1 : -1; - } else { - return row1.rowIndex > row2.rowIndex ? 1 : -1; - } - } else { - return row1.viewPortId > row2.viewPortId ? 1 : -1; - } -}; - type PendingRequest = { reject: (err: unknown) => void; resolve: (value: T | PromiseLike) => void; @@ -786,74 +775,49 @@ export class ServerProxy { break; case Message.TABLE_ROW: { - body.rows.sort(byViewportRowIdxTimestamp); - let currentViewportId = ""; - let viewport: Viewport | undefined; - let startIdx = 0; - - if (process.env.NODE_ENV === "development") { - if (debugEnabled) { - const [firstRow, secondRow] = body.rows; - if (body.rows.length === 0) { - debug("handleMessageFromServer TABLE_ROW 0 rows"); - } else if (firstRow?.rowIndex === -1) { - if (body.rows.length === 1) { - if (firstRow.updateType === "SIZE") { - debug( - `handleMessageFromServer [${firstRow.viewPortId}] TABLE_ROW SIZE ONLY ${firstRow.vpSize}` - ); - } else { - debug( - `handleMessageFromServer [${firstRow.viewPortId}] TABLE_ROW SIZE ${firstRow.vpSize} rowIdx ${firstRow.rowIndex}` - ); - } + const viewportRowMap = groupRowsByViewport(body.rows); + + if (process.env.NODE_ENV === "development" && debugEnabled) { + const [firstRow, secondRow] = body.rows; + if (body.rows.length === 0) { + debug("handleMessageFromServer TABLE_ROW 0 rows"); + } else if (firstRow?.rowIndex === -1) { + if (body.rows.length === 1) { + if (firstRow.updateType === "SIZE") { + debug( + `handleMessageFromServer [${firstRow.viewPortId}] TABLE_ROW SIZE ONLY ${firstRow.vpSize}` + ); } else { debug( - `handleMessageFromServer TABLE_ROW ${ - body.rows.length - } rows, SIZE ${firstRow.vpSize}, [${ - secondRow?.rowIndex - }] - [${body.rows[body.rows.length - 1]?.rowIndex}]` + `handleMessageFromServer [${firstRow.viewPortId}] TABLE_ROW SIZE ${firstRow.vpSize} rowIdx ${firstRow.rowIndex}` ); } } else { debug( `handleMessageFromServer TABLE_ROW ${ body.rows.length - } rows [${firstRow?.rowIndex}] - [${ - body.rows[body.rows.length - 1]?.rowIndex - }]` + } rows, SIZE ${firstRow.vpSize}, [${ + secondRow?.rowIndex + }] - [${body.rows[body.rows.length - 1]?.rowIndex}]` ); } + } else { + debug( + `handleMessageFromServer TABLE_ROW ${body.rows.length} rows [${ + firstRow?.rowIndex + }] - [${body.rows[body.rows.length - 1]?.rowIndex}]` + ); } } - for ( - let i = 0, count = body.rows.length, isLast = i === count - 1; - i < count; - i++, isLast = i === count - 1 - ) { - const row = body.rows[i]; - if (row.viewPortId !== currentViewportId || isLast) { - const viewportId = - count === 1 ? row.viewPortId : currentViewportId; - if (viewportId !== "") { - viewport = viewports.get(viewportId); - if (viewport) { - if (startIdx === 0 && isLast) { - viewport.updateRows(body.rows); - } else { - const end = isLast ? count : i; - viewport.updateRows(body.rows.slice(startIdx, end)); - startIdx = i; - } - } else { - warn?.( - `TABLE_ROW message received for non registered viewport ${viewportId}` - ); - } - } - currentViewportId = row.viewPortId; + for (const [viewportId, rows] of Object.entries(viewportRowMap)) { + const viewport = viewports.get(viewportId); + if (viewport) { + viewport.updateRows(rows); + } else { + warn?.( + `TABLE_ROW message received for non registered viewport ${viewportId}` + ); } } diff --git a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts index dc5578072..7876bddad 100644 --- a/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts +++ b/vuu-ui/packages/vuu-data/src/server-proxy/viewport.ts @@ -168,8 +168,6 @@ export class Viewport { private batchMode = true; private useBatchMode = true; - private ignorePostFilterRepeats = false; - private rangeMonitor = new RangeMonitor("ViewPort"); public clientViewportId: string; @@ -632,8 +630,6 @@ export class Viewport { data: dataSourceFilter, }); - this.ignorePostFilterRepeats = true; - if (this.useBatchMode) { this.batchMode = true; } @@ -745,16 +741,6 @@ export class Viewport { this.removePendingRangeRequest(firstRow.rowIndex, lastRow.rowIndex); } - if (this.ignorePostFilterRepeats) { - if (firstRow.vpSize === this.dataWindow?.rowCount) { - debug?.(`ignore data, rows are post filter repeats`); - this.ignorePostFilterRepeats = false; - return; - } else { - this.ignorePostFilterRepeats = false; - } - } - if (rows.length === 1 && firstRow.vpSize === 0 && this.disabled) { debug?.( `ignore a SIZE=0 message on disabled viewport (${rows.length} rows)` diff --git a/vuu-ui/packages/vuu-data/test/message-utils.test.ts b/vuu-ui/packages/vuu-data/test/message-utils.test.ts new file mode 100644 index 000000000..c12b4592a --- /dev/null +++ b/vuu-ui/packages/vuu-data/test/message-utils.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "vitest"; +import { createTableRows } from "./test-utils"; + +import { groupRowsByViewport } from "../src/message-utils"; + +describe("message-utils", () => { + describe("groupRowsByViewport", () => { + it("returns empty map for empty rowset", () => { + expect(groupRowsByViewport([])).toEqual({}); + }); + it("preserves order of rows in map by viewport, simple rowset", () => { + const rows = createTableRows("server-vp-1", 0, 10); + expect(groupRowsByViewport(rows)).toEqual({ + "server-vp-1": rows, + }); + }); + + it("preserves order of rows in map by viewport, multiple rowsets, same viewport", () => { + const rows1 = createTableRows("server-vp-1", 0, 10); + const rows2 = createTableRows("server-vp-1", 0, 10); + expect(groupRowsByViewport(rows1.concat(rows2))).toEqual({ + "server-vp-1": rows1.concat(rows2), + }); + }); + + it("preserves order of rows in map by viewport, multiple rowsets, multiple viewports", () => { + const rows1 = createTableRows("server-vp-1", 0, 10); + const rows2 = createTableRows("server-vp-2", 0, 10); + expect(groupRowsByViewport(rows1.concat(rows2))).toEqual({ + "server-vp-1": rows1, + "server-vp-2": rows2, + }); + }); + + it("preserves order of rows in map by viewport, multiple rowsets, multiple viewports, interleaved", () => { + const rows1 = createTableRows("server-vp-1", 0, 10); + const rows2 = createTableRows("server-vp-2", 0, 10); + const rows = rows1 + .concat(rows2) + .sort(({ rowIndex: i1 }, { rowIndex: i2 }) => i1 - i2); + expect(groupRowsByViewport(rows)).toEqual({ + "server-vp-1": rows1, + "server-vp-2": rows2, + }); + }); + }); +}); diff --git a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts index 0d966e054..8c86380b6 100644 --- a/vuu-ui/packages/vuu-data/test/server-proxy.test.ts +++ b/vuu-ui/packages/vuu-data/test/server-proxy.test.ts @@ -1663,6 +1663,219 @@ describe("ServerProxy", () => { }); }); + describe("filtering", () => { + it("invokes filter on viewport, which stores current filter criteria", () => { + const postMessageToClient = vi.fn(); + const serverProxy = + createServerProxyAndSubscribeToViewport(postMessageToClient); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + body: { + ...COMMON_TABLE_ROW_ATTRS, + rows: [sizeRow(), ...createTableRows("server-vp-1", 0, 10)], + }, + }); + + TEST_setRequestId(1); + postMessageToClient.mockClear(); + mockConnection.send.mockClear(); + + serverProxy.handleMessageFromClient({ + viewport: "client-vp-1", + type: "filter", + filter: { filter: 'ccy = "EUR"' }, + }); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + requestId: "1", + body: { + aggregations: [], + columns: ["col-1", "col-2", "col-3", "col-4"], + sort: { sortDefs: [] }, + filterSpec: { filter: 'ccy = "EUR"' }, + groupBy: [], + type: "CHANGE_VP_SUCCESS", + viewPortId: "server-vp-1", + }, + }); + + expect(serverProxy["viewports"].get("server-vp-1")?.["filter"]).toEqual({ + filter: 'ccy = "EUR"', + }); + }); + + it("sets batch mode when a filter has been applied", () => { + const postMessageToClient = vi.fn(); + const serverProxy = + createServerProxyAndSubscribeToViewport(postMessageToClient); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + body: { + ...COMMON_TABLE_ROW_ATTRS, + rows: [sizeRow(), ...createTableRows("server-vp-1", 0, 10)], + }, + }); + + TEST_setRequestId(1); + postMessageToClient.mockClear(); + mockConnection.send.mockClear(); + + serverProxy.handleMessageFromClient({ + viewport: "client-vp-1", + type: "filter", + filter: { filter: 'ccy = "EUR"' }, + }); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + requestId: "1", + body: { + aggregations: [], + columns: ["col-1", "col-2", "col-3", "col-4"], + sort: { sortDefs: [] }, + filterSpec: { filter: 'ccy = "EUR"' }, + groupBy: [], + type: "CHANGE_VP_SUCCESS", + viewPortId: "server-vp-1", + }, + }); + + postMessageToClient.mockClear(); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + body: { + ...COMMON_TABLE_ROW_ATTRS, + rows: [ + sizeRow("server-vp-1", 43), + updateTableRow("server-vp-1", 0, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 1, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 2, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 3, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 4, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 5, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 6, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 7, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 8, 1234, { vpSize: 43 }), + updateTableRow("server-vp-1", 9, 1234, { vpSize: 43 }), + ], + }, + }); + expect(postMessageToClient).toHaveBeenCalledTimes(1); + // prettier-ignore + expect(postMessageToClient).toHaveBeenNthCalledWith<[DataSourceDataMessage]>(1, { + type: "viewport-update", + mode: "batch", + clientViewportId: "client-vp-1", + rows: [ + [0,0,true,false,0,0,"key-00",0,"key-00","name 00",1234, true], + [1,1,true,false,0,0,"key-01",0,"key-01","name 01",1234, true], + [2,2,true,false,0,0,"key-02",0,"key-02","name 02",1234, true], + [3,3,true,false,0,0,"key-03",0,"key-03","name 03",1234, true], + [4,4,true,false,0,0,"key-04",0,"key-04","name 04",1234, true], + [5,5,true,false,0,0,"key-05",0,"key-05","name 05",1234, true], + [6,6,true,false,0,0,"key-06",0,"key-06","name 06",1234, true], + [7,7,true,false,0,0,"key-07",0,"key-07","name 07",1234, true], + [8,8,true,false,0,0,"key-08",0,"key-08","name 08",1234, true], + [9,9,true,false,0,0,"key-09",0,"key-09","name 09",1234, true], + ], + size: 43 + }); + }); + it("handles TABLE_ROWS that preceed filter request together with filtered rows, in same batch", () => { + const postMessageToClient = vi.fn(); + const serverProxy = + createServerProxyAndSubscribeToViewport(postMessageToClient); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + body: { + ...COMMON_TABLE_ROW_ATTRS, + rows: [sizeRow(), ...createTableRows("server-vp-1", 0, 10)], + }, + }); + + TEST_setRequestId(1); + postMessageToClient.mockClear(); + mockConnection.send.mockClear(); + + serverProxy.handleMessageFromClient({ + viewport: "client-vp-1", + type: "filter", + filter: { filter: 'ccy = "EUR"' }, + }); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + requestId: "1", + body: { + aggregations: [], + columns: ["col-1", "col-2", "col-3", "col-4"], + sort: { sortDefs: [] }, + filterSpec: { filter: 'ccy = "EUR"' }, + groupBy: [], + type: "CHANGE_VP_SUCCESS", + viewPortId: "server-vp-1", + }, + }); + + postMessageToClient.mockClear(); + + serverProxy.handleMessageFromServer({ + ...COMMON_ATTRS, + body: { + ...COMMON_TABLE_ROW_ATTRS, + rows: [ + updateTableRow("server-vp-1", 0, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 1, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 2, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 3, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 4, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 5, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 6, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 7, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 8, 1234, { vpSize: 100 }), + updateTableRow("server-vp-1", 9, 1234, { vpSize: 100 }), + sizeRow("server-vp-1", 43), + updateTableRow("server-vp-1", 0, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 1, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 2, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 3, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 4, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 5, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 6, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 7, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 8, 2004, { vpSize: 43 }), + updateTableRow("server-vp-1", 9, 2004, { vpSize: 43 }), + ], + }, + }); + expect(postMessageToClient).toHaveBeenCalledTimes(1); + // prettier-ignore + expect(postMessageToClient).toHaveBeenNthCalledWith<[DataSourceDataMessage]>(1, { + type: "viewport-update", + mode: "batch", + clientViewportId: "client-vp-1", + rows: [ + [0,0,true,false,0,0,"key-00",0,"key-00","name 00",2004, true], + [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2004, true], + [2,2,true,false,0,0,"key-02",0,"key-02","name 02",2004, true], + [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2004, true], + [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2004, true], + [5,5,true,false,0,0,"key-05",0,"key-05","name 05",2004, true], + [6,6,true,false,0,0,"key-06",0,"key-06","name 06",2004, true], + [7,7,true,false,0,0,"key-07",0,"key-07","name 07",2004, true], + [8,8,true,false,0,0,"key-08",0,"key-08","name 08",2004, true], + [9,9,true,false,0,0,"key-09",0,"key-09","name 09",2004, true], + ], + size: 43 + }); + }); + }); + describe("GroupBy", () => { const [clientSubscription1, serverSubscriptionAck1] = createSubscription(); @@ -2966,8 +3179,8 @@ describe("ServerProxy", () => { mode: "update", clientViewportId: "client-vp-1", rows: [ - [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2003, true], [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2003, true], + [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2003, true], ], }); // prettier-ignore @@ -2977,8 +3190,8 @@ describe("ServerProxy", () => { clientViewportId: "client-vp-2", rows: [ [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2003, true], - [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2003, true], [5,5,true,false,0,0,"key-05",0,"key-05","name 05",2003, true], + [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2003, true], ], }); }); @@ -3045,8 +3258,8 @@ describe("ServerProxy", () => { mode: "update", clientViewportId: "client-vp-1", rows: [ - [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2003, true], [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2003, true], + [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2003, true], ], size: 50 }); @@ -3057,8 +3270,8 @@ describe("ServerProxy", () => { clientViewportId: "client-vp-2", rows: [ [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2003, true], - [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2003, true], [5,5,true,false,0,0,"key-05",0,"key-05","name 05",2003, true], + [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2003, true], ], size: undefined }); @@ -3087,8 +3300,8 @@ describe("ServerProxy", () => { mode: "update", clientViewportId: "client-vp-1", rows: [ - [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2004, true], [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2004, true], + [1,1,true,false,0,0,"key-01",0,"key-01","name 01",2004, true], ], size: undefined }); @@ -3099,8 +3312,8 @@ describe("ServerProxy", () => { clientViewportId: "client-vp-2", rows: [ [3,3,true,false,0,0,"key-03",0,"key-03","name 03",2004, true], - [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2004, true], [5,5,true,false,0,0,"key-05",0,"key-05","name 05",2004, true], + [4,4,true,false,0,0,"key-04",0,"key-04","name 04",2004, true], ], size: 75 });