From 93695f5a4b559c3cdd006567225e47c13bb91134 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Tue, 28 Jul 2026 15:58:08 +0800 Subject: [PATCH 1/5] Update the Protobuf and ICD version numbers in the configuration file --- protobuf | 2 +- src/test/config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/protobuf b/protobuf index 9b181d2..cd18d38 160000 --- a/protobuf +++ b/protobuf @@ -1 +1 @@ -Subproject commit 9b181d2811807bec2af667eb5330aa6567bbf428 +Subproject commit cd18d38a5a649c3df2865330457afb54c5392201 diff --git a/src/test/config.json b/src/test/config.json index 1902afa..1f91f91 100644 --- a/src/test/config.json +++ b/src/test/config.json @@ -1,6 +1,6 @@ { "serverURL0": "ws://127.0.0.1:3002", - "icdVersion": 30, + "icdVersion": 31, "path": { "root": ".", "base": "$BASE", From 51c34e922b6af5fdc6e8df1a138fa7749e829b49 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 29 Jul 2026 00:47:49 +0800 Subject: [PATCH 2/5] Update CHANNEL_MAP test for per-channel flow control protocol --- src/test/CHANNEL_MAP.test.ts | 572 ++++++++-------------------------- src/test/MessageController.ts | 17 +- src/test/MyClient.ts | 33 +- 3 files changed, 167 insertions(+), 455 deletions(-) diff --git a/src/test/CHANNEL_MAP.test.ts b/src/test/CHANNEL_MAP.test.ts index 36b8272..0e1a321 100644 --- a/src/test/CHANNEL_MAP.test.ts +++ b/src/test/CHANNEL_MAP.test.ts @@ -13,11 +13,19 @@ interface AssertItem { fileList: CARTA.IFileListRequest; fileOpenGroup: CARTA.IOpenFile[]; fileOpenAckGroup: CARTA.IOpenFileAck[]; - setImageChannelGroup: CARTA.ISetImageChannels[]; - setImageChannelGroup2: CARTA.ISetImageChannels[]; addRequiredTilesGroup: CARTA.IAddRequiredTiles[]; rasterTileDataGroup: CARTA.IRasterTileData[]; - tileDataGroup: CARTA.ITileData[]; + // The channels rendered, one request at a time, for the first channel map view + channelMapView: number[]; + // A subsequent single channel jump within the channel map view + channelMapJump: number; + tileData: { + layer: number; + x: number[]; + y: number[]; + width: number[]; + height: number; + }; } const requiredTiles = [ @@ -25,10 +33,6 @@ const requiredTiles = [ 33566722, ]; const tiles = [33558529, 33558528, 33562625, 33554433, 33562624, 33558530, 33554432, 33562626, 33554434]; -const currentTiles = [33554432, 33558528, 33562624, 33554433, 33558529, 33562625, 33554434, 33558530, 33562626]; -const assertTileXY = [0, 1, 2]; -const assertTileWidth = [128, 256]; -const assertTileHeight = 256; const assertItem: AssertItem = { fileList: { directory: testSubdirectory }, @@ -63,229 +67,58 @@ const assertItem: AssertItem = { }, ], fileOpenAckGroup: [ - { - success: true, - fileId: 0, - }, - { - success: true, - fileId: 1, - }, - { - success: true, - fileId: 2, - }, - { - success: true, - fileId: 3, - }, - ], - setImageChannelGroup: [ - { - fileId: 0, - channel: 0, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 1, max: 3 }, - currentRange: { min: 0, max: 3 }, - channelMapEnabled: true, - }, - { - fileId: 1, - channel: 0, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 1, max: 3 }, - currentRange: { min: 0, max: 3 }, - channelMapEnabled: true, - }, - { - fileId: 2, - channel: 0, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 1, max: 3 }, - currentRange: { min: 0, max: 3 }, - channelMapEnabled: true, - }, - { - fileId: 3, - channel: 0, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 1, max: 3 }, - currentRange: { min: 0, max: 3 }, - channelMapEnabled: true, - }, - ], - setImageChannelGroup2: [ - { - fileId: 0, - channel: 4, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 4, max: 4 }, - currentRange: { min: 1, max: 4 }, - channelMapEnabled: true, - }, - { - fileId: 1, - channel: 4, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 4, max: 4 }, - currentRange: { min: 1, max: 4 }, - channelMapEnabled: true, - }, - { - fileId: 2, - channel: 4, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 4, max: 4 }, - currentRange: { min: 1, max: 4 }, - channelMapEnabled: true, - }, - { - fileId: 3, - channel: 4, - stokes: 0, - requiredTiles: { - fileId: 0, - tiles: tiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - currentTiles: currentTiles, - }, - channelRange: { min: 4, max: 4 }, - currentRange: { min: 1, max: 4 }, - channelMapEnabled: true, - }, + { success: true, fileId: 0 }, + { success: true, fileId: 1 }, + { success: true, fileId: 2 }, + { success: true, fileId: 3 }, ], addRequiredTilesGroup: [ - { - fileId: 0, - tiles: requiredTiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - }, - { - fileId: 1, - tiles: requiredTiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - }, - { - fileId: 2, - tiles: requiredTiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - }, - { - fileId: 3, - tiles: requiredTiles, - compressionType: CARTA.CompressionType.ZFP, - compressionQuality: 11, - }, + { fileId: 0, tiles: requiredTiles, compressionType: CARTA.CompressionType.ZFP, compressionQuality: 11 }, + { fileId: 1, tiles: requiredTiles, compressionType: CARTA.CompressionType.ZFP, compressionQuality: 11 }, + { fileId: 2, tiles: requiredTiles, compressionType: CARTA.CompressionType.ZFP, compressionQuality: 11 }, + { fileId: 3, tiles: requiredTiles, compressionType: CARTA.CompressionType.ZFP, compressionQuality: 11 }, ], rasterTileDataGroup: [ - { - fileId: 0, - channel: 0, - stokes: 0, - }, - { - fileId: 1, - channel: 0, - stokes: 0, - }, - { - fileId: 2, - channel: 0, - stokes: 0, - }, - { - fileId: 3, - channel: 0, - stokes: 0, - }, - ], - tileDataGroup: [ - { - layer: 2, - x: 2, - y: 0, - width: 128, - height: 256, - }, - { - layer: 2, - x: 2, - y: 0, - width: 128, - height: 256, - }, - { - layer: 2, - x: 2, - y: 0, - width: 128, - height: 256, - }, - { - layer: 2, - x: 2, - y: 0, - width: 128, - height: 256, - }, + { fileId: 0, channel: 0, stokes: 0 }, + { fileId: 1, channel: 0, stokes: 0 }, + { fileId: 2, channel: 0, stokes: 0 }, + { fileId: 3, channel: 0, stokes: 0 }, ], + channelMapView: [1, 2, 3], + channelMapJump: 4, + tileData: { + layer: 2, + x: [0, 1, 2], + y: [0, 1, 2], + width: [128, 256], + height: 256, + }, }; +// Build a single-channel SET_IMAGE_CHANNELS request for the channel map view. +// Since the refactor, the backend processes one channel per request and acknowledges each +// with a CHANNEL_MAP_FLOW_CONTROL message; the frontend sends the next channel only after the +// previous one completes. +function channelMapRequest(fileId: number, channel: number): CARTA.ISetImageChannels { + return { + fileId, + channel, + stokes: 0, + requiredTiles: { + fileId, + tiles, + compressionType: CARTA.CompressionType.ZFP, + compressionQuality: 11, + }, + channelMapEnabled: true, + }; +} + describe('CHANNEL_MAP: Test loading multiple images and generating their channel maps', () => { const msgController = MessageController.Instance; + const rasterTileDataLen = tiles.length; // one RasterTileData per tile + const rasterTileMsgLen = rasterTileDataLen + 2; // + RasterTileSync start & end + describe(`Register a session`, () => { beforeAll(async () => { await msgController.connect(testServerUrl); @@ -302,6 +135,8 @@ describe('CHANNEL_MAP: Test loading multiple images and generating their channel describe(`Go to "${assertItem.fileList.directory}" folder`, () => { assertItem.fileOpenGroup.map((input, index) => { + const fileId = assertItem.fileOpenAckGroup[index].fileId as number; + describe(`Open ${input.file}`, () => { let OpenFileAck: any; test( @@ -358,230 +193,81 @@ describe('CHANNEL_MAP: Test loading multiple images and generating their channel }); }); - describe(`1) Set image channel map for the file "${assertItem.fileOpenGroup[index].file}"`, () => { - let RasterTileMsgTemp: any; - const rasterTileDataLen = assertItem.setImageChannelGroup[index].requiredTiles.tiles.length; - const rasterTileMsgLen = rasterTileDataLen + 2; - - test( - `Three RASTER_TILE_DATA should arrive within ${readFileTimeout} ms`, - async () => { - msgController.setChannels(assertItem.setImageChannelGroup[index]); - const maxChan = assertItem.setImageChannelGroup[index].channelRange.max; - const minChan = assertItem.setImageChannelGroup[index].channelRange.min; - const channels = maxChan - minChan + 1; - RasterTileMsgTemp = await ChannelMapStream(rasterTileDataLen, channels); - }, - readFileTimeout - ); - - // Check file Ids - test(`1st RASTER_TILE_DATA.file_id = ${assertItem.rasterTileDataGroup[index].fileId}`, () => { - expect(RasterTileMsgTemp[1].fileId).toEqual(assertItem.rasterTileDataGroup[index].fileId); - }); - test(`2nd RASTER_TILE_DATA.file_id = ${assertItem.rasterTileDataGroup[index].fileId}`, () => { - expect(RasterTileMsgTemp[rasterTileMsgLen + 1].fileId).toEqual( - assertItem.rasterTileDataGroup[index].fileId - ); - }); - test(`3rd RASTER_TILE_DATA.file_id = ${assertItem.rasterTileDataGroup[index].fileId}`, () => { - expect(RasterTileMsgTemp[2 * rasterTileMsgLen + 1].fileId).toEqual( - assertItem.rasterTileDataGroup[index].fileId - ); - }); - - // Check channels - test(`1st RASTER_TILE_DATA.channel = ${assertItem.rasterTileDataGroup[index].channel + 1}`, () => { - expect(RasterTileMsgTemp[1].channel).toEqual(assertItem.rasterTileDataGroup[index].channel + 1); - }); - test(`2nd RASTER_TILE_DATA.channel = ${assertItem.rasterTileDataGroup[index].channel + 2}`, () => { - expect(RasterTileMsgTemp[rasterTileMsgLen + 1].channel).toEqual( - assertItem.rasterTileDataGroup[index].channel + 2 + // Render the channel map view one channel at a time. Each request is answered by + // that channel's raster tiles followed by a CHANNEL_MAP_FLOW_CONTROL ack; the next + // request is only sent after the previous channel completes. + const channelMapSequence = [...assertItem.channelMapView, assertItem.channelMapJump]; + + channelMapSequence.forEach((channel, seq) => { + const label = seq < assertItem.channelMapView.length ? `view channel ${channel}` : `jump to channel ${channel}`; + + describe(`Channel map (${label}) for the file "${assertItem.fileOpenGroup[index].file}"`, () => { + let response: { rasterTileMsgs: any[]; flowControl: any }; + + test( + `RASTER_TILE_DATA and CHANNEL_MAP_FLOW_CONTROL should arrive within ${readFileTimeout} ms`, + async () => { + msgController.setChannels(channelMapRequest(fileId, channel)); + response = await ChannelMapStream(rasterTileDataLen); + // one channel = # tiles + 2 RasterTileSync + expect(response.rasterTileMsgs.length).toEqual(rasterTileMsgLen); + }, + readFileTimeout ); - }); - test(`3rd RASTER_TILE_DATA.channel = ${assertItem.rasterTileDataGroup[index].channel + 3}`, () => { - expect(RasterTileMsgTemp[2 * rasterTileMsgLen + 1].channel).toEqual( - assertItem.rasterTileDataGroup[index].channel + 3 - ); - }); - - // Check stokes - test(`1st RASTER_TILE_DATA.stokes = ${assertItem.rasterTileDataGroup[index].stokes}`, () => { - expect(RasterTileMsgTemp[1].stokes).toEqual(assertItem.rasterTileDataGroup[index].stokes); - }); - test(`2nd RASTER_TILE_DATA.stokes = ${assertItem.rasterTileDataGroup[index].stokes}`, () => { - expect(RasterTileMsgTemp[rasterTileMsgLen + 1].stokes).toEqual( - assertItem.rasterTileDataGroup[index].stokes - ); - }); - test(`3rd RASTER_TILE_DATA.stokes = ${assertItem.rasterTileDataGroup[index].stokes}`, () => { - expect(RasterTileMsgTemp[2 * rasterTileMsgLen + 1].stokes).toEqual( - assertItem.rasterTileDataGroup[index].stokes - ); - }); - - // Check tiles - const idx1 = rasterTileMsgLen - 1 - 1; - const idx2 = 2 * rasterTileMsgLen - 1 - 1; - const idx3 = 3 * rasterTileMsgLen - 1 - 1; - - // Check tiles length - test(`1st RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx1].tiles.length).toEqual(1); - }); - test(`2nd RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx2].tiles.length).toEqual(1); - }); - test(`3rd RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx3].tiles.length).toEqual(1); - }); - - // Check tiles layer - test(`1st RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx1].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - test(`2nd RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx2].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - test(`3rd RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx3].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - - // Check tiles x - test(`1st RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - - // Check tiles y - test(`1st RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - - // Check tiles width - test(`1st RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - - // Check tiles height - test(`1st RASTER_TILE_DATA.tiles.height = ${assertTileHeight}`, () => { - for (let i = idx1; i > idx1 - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } - }); - test(`2nd RASTER_TILE_DATA.tiles.height = ${assertTileHeight}`, () => { - for (let i = idx2; i > idx2 - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } - }); - test(`3rd RASTER_TILE_DATA.tiles.height = ${assertItem.tileDataGroup[index].height}`, () => { - for (let i = idx3; i > idx3 - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } - }); - }); - - describe(`2) Set image channel map for the file "${assertItem.fileOpenGroup[index].file}"`, () => { - let RasterTileMsgTemp: any; - const rasterTileDataLen = assertItem.setImageChannelGroup2[index].requiredTiles.tiles.length; - const resterTileMsgLen = rasterTileDataLen + 2; - - test( - `A new RASTER_TILE_DATA should arrive within ${readFileTimeout} ms`, - async () => { - msgController.setChannels(assertItem.setImageChannelGroup2[index]); - const maxChan = assertItem.setImageChannelGroup2[index].channelRange.max; - const minChan = assertItem.setImageChannelGroup2[index].channelRange.min; - const channels = maxChan - minChan + 1; - RasterTileMsgTemp = await ChannelMapStream(rasterTileDataLen, channels); - }, - readFileTimeout - ); - - // Check channels - test(`New RASTER_TILE_DATA.channel = ${assertItem.setImageChannelGroup2[index].channel}`, () => { - expect(RasterTileMsgTemp[1].channel).toEqual(assertItem.setImageChannelGroup2[index].channel); - }); - - // Check stokes - test(`New RASTER_TILE_DATA.stokes = ${assertItem.setImageChannelGroup2[index].stokes}`, () => { - expect(RasterTileMsgTemp[1].stokes).toEqual(assertItem.setImageChannelGroup2[index].stokes); - }); - - // Check tiles - const idx = resterTileMsgLen - 1 - 1; - - // Check tiles length - test(`New RASTER_TILE_DATA.tiles.length = 1`, () => { - expect(RasterTileMsgTemp[idx].tiles.length).toEqual(1); - }); - - // Check tiles layer - test(`New RASTER_TILE_DATA.tiles.layer = ${assertItem.tileDataGroup[index].layer}`, () => { - expect(RasterTileMsgTemp[idx].tiles[0].layer).toEqual(assertItem.tileDataGroup[index].layer); - }); - - // Check tiles x - test(`New RASTER_TILE_DATA.tiles.x = ${assertTileXY}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].x); - } - }); - - // Check tiles y - test(`New RASTER_TILE_DATA.tiles.y = ${assertTileXY}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(assertTileXY).toContain(RasterTileMsgTemp[i].tiles[0].y); - } - }); - - // Check tiles width - test(`New RASTER_TILE_DATA.tiles.width = ${assertTileWidth}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(assertTileWidth).toContain(RasterTileMsgTemp[i].tiles[0].width); - } - }); - // Check tiles height - test(`New RASTER_TILE_DATA.tiles.height = ${assertTileHeight}`, () => { - for (let i = idx; i > idx - rasterTileDataLen; i--) { - expect(RasterTileMsgTemp[i].tiles[0].height).toEqual(assertTileHeight); - } + // Check the flow control acknowledgment + test(`CHANNEL_MAP_FLOW_CONTROL.file_id = ${fileId}`, () => { + expect(response.flowControl.fileId).toEqual(fileId); + }); + test(`CHANNEL_MAP_FLOW_CONTROL.completed_channel = ${channel}`, () => { + expect(response.flowControl.completedChannel).toEqual(channel); + }); + test(`CHANNEL_MAP_FLOW_CONTROL.status = COMPLETED`, () => { + expect(response.flowControl.status).toEqual(CARTA.ChannelMapFlowControl.Status.COMPLETED); + }); + + // Check channel / stokes of the raster tiles (first RasterTileData follows the sync start) + test(`RASTER_TILE_DATA.file_id = ${fileId}`, () => { + expect(response.rasterTileMsgs[1].fileId).toEqual(fileId); + }); + test(`RASTER_TILE_DATA.channel = ${channel}`, () => { + expect(response.rasterTileMsgs[1].channel).toEqual(channel); + }); + test(`RASTER_TILE_DATA.stokes = 0`, () => { + expect(response.rasterTileMsgs[1].stokes).toEqual(0); + }); + + // Check tiles (RasterTileData occupy indices 1 .. rasterTileDataLen) + test(`each RASTER_TILE_DATA.tiles.length = 1`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(response.rasterTileMsgs[i].tiles.length).toEqual(1); + } + }); + test(`each RASTER_TILE_DATA.tiles.layer = ${assertItem.tileData.layer}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(response.rasterTileMsgs[i].tiles[0].layer).toEqual(assertItem.tileData.layer); + } + }); + test(`each RASTER_TILE_DATA.tiles.x in ${assertItem.tileData.x}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(assertItem.tileData.x).toContain(response.rasterTileMsgs[i].tiles[0].x); + } + }); + test(`each RASTER_TILE_DATA.tiles.y in ${assertItem.tileData.y}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(assertItem.tileData.y).toContain(response.rasterTileMsgs[i].tiles[0].y); + } + }); + test(`each RASTER_TILE_DATA.tiles.width in ${assertItem.tileData.width}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(assertItem.tileData.width).toContain(response.rasterTileMsgs[i].tiles[0].width); + } + }); + test(`each RASTER_TILE_DATA.tiles.height = ${assertItem.tileData.height}`, () => { + for (let i = 1; i <= rasterTileDataLen; i++) { + expect(response.rasterTileMsgs[i].tiles[0].height).toEqual(assertItem.tileData.height); + } + }); }); }); }); diff --git a/src/test/MessageController.ts b/src/test/MessageController.ts index b1971f6..d0962a3 100644 --- a/src/test/MessageController.ts +++ b/src/test/MessageController.ts @@ -94,6 +94,7 @@ export class MessageController { readonly fittingProgressStream: Subject; readonly vectorTileStream: Subject; readonly pvPreviewStream: Subject; + readonly channelMapFlowControlStream: Subject; private readonly decoderMap: Map; private constructor() { @@ -123,6 +124,7 @@ export class MessageController { this.fittingProgressStream = new Subject(); this.vectorTileStream = new Subject(); this.pvPreviewStream = new Subject(); + this.channelMapFlowControlStream = new Subject(); // Construct handler and decoder maps this.decoderMap = new Map([ @@ -378,6 +380,13 @@ export class MessageController { handler: this.onDeferredResponse, }, ], + [ + CARTA.EventType.CHANNEL_MAP_FLOW_CONTROL, + { + messageClass: CARTA.ChannelMapFlowControl, + handler: this.onStreamedChannelMapFlowControl, + }, + ], ]); // check ping every 5 seconds @@ -848,16 +857,12 @@ export class MessageController { let stokes = input.stokes; let requiredTiles = input.requiredTiles; let channelMapEnabled = input.channelMapEnabled; - let channelRange = input.channelRange; - let currentRange = input.currentRange; if (this.connectionStatus === ConnectionStatus.ACTIVE) { const message = CARTA.SetImageChannels.create({ fileId: fileId, channel: channel, stokes: stokes, requiredTiles: requiredTiles, - channelRange: channelRange, - currentRange: currentRange, channelMapEnabled: channelMapEnabled, }); this.logEvent(CARTA.EventType.SET_IMAGE_CHANNELS, this.eventCounter, message, false); @@ -1468,6 +1473,10 @@ export class MessageController { this.pvPreviewStream.next(previewData); } + private onStreamedChannelMapFlowControl(_eventId: number, flowControl: CARTA.ChannelMapFlowControl) { + this.channelMapFlowControlStream.next(flowControl); + } + private sendEvent(eventType: CARTA.EventType, payload: Uint8Array): boolean { if (this.connection.readyState === WebSocket.OPEN) { const eventData = new Uint8Array(8 + payload.byteLength); diff --git a/src/test/MyClient.ts b/src/test/MyClient.ts index a295366..176695b 100644 --- a/src/test/MyClient.ts +++ b/src/test/MyClient.ts @@ -155,24 +155,41 @@ function Stream(cartaType: any, InputNum?: number) { }); } -function ChannelMapStream(rasterTileDataLen: number, channels: number) { - return new Promise((resolve, reject) => { +// Collects the response to a single channel map request. Since the refactor, the backend +// processes one channel per SET_IMAGE_CHANNELS request and replies with that channel's raster +// tiles (2 RasterTileSync + rasterTileDataLen RasterTileData) followed by a single +// CHANNEL_MAP_FLOW_CONTROL message reporting the request outcome. Resolves once both the tile +// batch and the flow control message have arrived. +function ChannelMapStream(rasterTileDataLen: number) { + return new Promise<{ rasterTileMsgs: any[]; flowControl: any }>((resolve, reject) => { const msgController = MessageController.Instance; - const rasterTileMsgLen = (rasterTileDataLen + 2) * channels; // # of RasterTileData + 2 RasterTileSync per channel + const rasterTileMsgLen = rasterTileDataLen + 2; // # of RasterTileData + 2 RasterTileSync for one channel let count = 0; let rasterTileMsgs: any[] = []; - let rasterTileSyncStream = msgController.rasterSyncStream.pipe(take(2 * channels)); // 2 RasterTileSync per channel + let flowControl: any = null; + + const tryResolve = () => { + if (count === rasterTileMsgLen && flowControl !== null) { + resolve({ rasterTileMsgs, flowControl }); + } + }; + + const rasterTileSyncStream = msgController.rasterSyncStream.pipe(take(2)); // start + end rasterTileSyncStream.subscribe((data) => { count++; rasterTileMsgs.push(data); - if (data.endSync && count === rasterTileMsgLen) { - resolve(rasterTileMsgs); - } + tryResolve(); }); - let rasterTileDataStream = msgController.rasterTileStream.pipe(take(rasterTileDataLen * channels)); + const rasterTileDataStream = msgController.rasterTileStream.pipe(take(rasterTileDataLen)); rasterTileDataStream.subscribe((data) => { count++; rasterTileMsgs.push(data); + tryResolve(); + }); + const flowControlStream = msgController.channelMapFlowControlStream.pipe(take(1)); + flowControlStream.subscribe((data) => { + flowControl = data; + tryResolve(); }); }); } From cc57437530f31378738a76a076a11d3f85fa36ca Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 29 Jul 2026 00:57:09 +0800 Subject: [PATCH 3/5] Remove the current_tiles field from AddRequiredTiles requests --- src/test/MessageController.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/MessageController.ts b/src/test/MessageController.ts index d0962a3..ba2f621 100644 --- a/src/test/MessageController.ts +++ b/src/test/MessageController.ts @@ -1035,14 +1035,12 @@ export class MessageController { let tiles = input.tiles; let type = input.compressionType; let quality = input.compressionQuality; - let currentTiles = input.currentTiles; if (this.connectionStatus === ConnectionStatus.ACTIVE) { const message = CARTA.AddRequiredTiles.create({ fileId: fileId, tiles: tiles, compressionType: type, compressionQuality: quality, - currentTiles: currentTiles, }); this.logEvent(CARTA.EventType.ADD_REQUIRED_TILES, this.eventCounter, message, false); if (this.sendEvent(CARTA.EventType.ADD_REQUIRED_TILES, CARTA.AddRequiredTiles.encode(message).finish())) { From 1ac38c02ccb46c2a16227e534adfb59d3baa5dc6 Mon Sep 17 00:00:00 2001 From: Cheng-Chin Chiang Date: Wed, 29 Jul 2026 01:39:33 +0800 Subject: [PATCH 4/5] Update the channel map documentation for per-channel flow control --- docs/source/channel_map.rst | 71 ++++++++++++++++++++++++------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/docs/source/channel_map.rst b/docs/source/channel_map.rst index cd426f2..6f13ca8 100644 --- a/docs/source/channel_map.rst +++ b/docs/source/channel_map.rst @@ -30,10 +30,15 @@ Channel Map User -> Frontend: Request channel map activate Frontend - Frontend -> Backend : 5. SET_IMAGE_CHANNELS (channel_map_enabled) - activate Backend - Frontend <--[#red] Backend : 6. RASTER_TILE_DATA [Check 1] (per channel) - deactivate Backend + + loop One request per channel + Frontend -> Backend : 5. SET_IMAGE_CHANNELS (channel_map_enabled) + activate Backend + Frontend <--[#red] Backend : 6. RASTER_TILE_DATA [Check 3] + Frontend <--[#red] Backend : 7. CHANNEL_MAP_FLOW_CONTROL [Check 4] + deactivate Backend + end + User <-- Frontend: Displays channel map deactivate Frontend @@ -44,6 +49,16 @@ See the `source code Date: Wed, 29 Jul 2026 02:03:38 +0800 Subject: [PATCH 5/5] Fix the code style --- src/test/CHANNEL_MAP.test.ts | 5 ++++- src/test/FILEINFO_EXCEPTIONS.test.ts | 2 +- src/test/MessageController.ts | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/test/CHANNEL_MAP.test.ts b/src/test/CHANNEL_MAP.test.ts index 0e1a321..831f6f0 100644 --- a/src/test/CHANNEL_MAP.test.ts +++ b/src/test/CHANNEL_MAP.test.ts @@ -199,7 +199,10 @@ describe('CHANNEL_MAP: Test loading multiple images and generating their channel const channelMapSequence = [...assertItem.channelMapView, assertItem.channelMapJump]; channelMapSequence.forEach((channel, seq) => { - const label = seq < assertItem.channelMapView.length ? `view channel ${channel}` : `jump to channel ${channel}`; + const label = + seq < assertItem.channelMapView.length + ? `view channel ${channel}` + : `jump to channel ${channel}`; describe(`Channel map (${label}) for the file "${assertItem.fileOpenGroup[index].file}"`, () => { let response: { rasterTileMsgs: any[]; flowControl: any }; diff --git a/src/test/FILEINFO_EXCEPTIONS.test.ts b/src/test/FILEINFO_EXCEPTIONS.test.ts index 57e2e50..1f15dbf 100644 --- a/src/test/FILEINFO_EXCEPTIONS.test.ts +++ b/src/test/FILEINFO_EXCEPTIONS.test.ts @@ -59,4 +59,4 @@ describe('FILEINFO_EXCEPTIONS: Testing error handle of file info generation', () }); afterAll(() => msgController.closeConnection()); }); -}); \ No newline at end of file +}); diff --git a/src/test/MessageController.ts b/src/test/MessageController.ts index ba2f621..bbd356b 100644 --- a/src/test/MessageController.ts +++ b/src/test/MessageController.ts @@ -421,7 +421,9 @@ export class MessageController { if (wasNeverActive) { const def = this.deferredMap.get(requestId); if (def) { - def.reject(new Error(`WebSocket closed before connect: code=${ev.code} reason=${ev.reason}`)); + def.reject( + new Error(`WebSocket closed before connect: code=${ev.code} reason=${ev.reason}`) + ); this.deferredMap.delete(requestId); } }