@@ -14,7 +14,7 @@ const version_id = 'dev',
1414
1515/** @summary version date
1616 * @desc Release date in format day/month/year like '14/04/2022' */
17- version_date = '12 /11/2025',
17+ version_date = '24 /11/2025',
1818
1919/** @summary version id and date
2020 * @desc Produced by concatenation of {@link version_id} and {@link version_date}
@@ -80315,32 +80315,42 @@ class StandaloneMenu extends JSRootMenu {
8031580315 select(`#${dlg_id}`).remove();
8031680316 select(`#${dlg_id}_block`).remove();
8031780317
80318- const w = Math.min(args.width || 450, Math.round(0.9 * browser.screenWidth));
80319- modal.block = select('body').append('div')
80320- .attr('id', `${dlg_id}_block` )
80321- .attr('class ', 'jsroot_dialog_block' )
80322- .attr('style ', 'z-index: 100000; position: absolute; left: 0px; top: 0px; bottom: 0px; right: 0px; opacity: 0.2; background-color: white');
80323- modal.element = select('body')
80324- .append('div')
80325- .attr('id', dlg_id)
80326- .attr('class', 'jsroot_dialog')
80327- .style('position', 'absolute')
80328- .style('width', `${w}px`)
80329- .style('left', '50%')
80330- .style('top', '50%')
80331- .style('z-index', 100001)
80332- .attr('tabindex', '0');
80318+ const w = Math.min(args.width || 450, Math.round(0.9 * browser.screenWidth)),
80319+ b = select('body');
80320+ modal.block = b.append('div' )
80321+ .attr('id ', `${dlg_id}_block` )
80322+ .attr('class ', 'jsroot_dialog_block')
80323+ .attr('style', 'z-index: 100000; position: absolute; left: 0px; top: 0px; bottom: 0px; right: 0px; opacity: 0.2; background-color: white');
80324+ modal.element = b .append('div')
80325+ .attr('id', dlg_id)
80326+ .attr('class', 'jsroot_dialog')
80327+ .style('position', 'absolute')
80328+ .style('width', `${w}px`)
80329+ .style('left', '50%')
80330+ .style('top', '50%')
80331+ .style('z-index', 100001)
80332+ .attr('tabindex', '0');
8033380333
8033480334 modal.element.html(
8033580335 '<div style=\'position: relative; left: -50%; top: -50%; border: solid green 3px; padding: 5px; display: flex; flex-flow: column; background-color: white\'>' +
80336- `<div style='flex: 0 1 auto; padding: 5px'>${title}</div>` +
80336+ `<div style='flex: 0 1 auto; padding: 5px; cursor: pointer;' class='jsroot_dialog_title '>${title}</div>` +
8033780337 `<div class='jsroot_dialog_content' style='flex: 1 1 auto; padding: 5px'>${main_content}</div>` +
8033880338 '<div class=\'jsroot_dialog_footer\' style=\'flex: 0 1 auto; padding: 5px\'>' +
8033980339 `<button class='jsroot_dialog_button' style='float: right; width: fit-content; margin-right: 1em'>${args.Ok}</button>` +
8034080340 (args.btns ? '<button class=\'jsroot_dialog_button\' style=\'float: right; width: fit-content; margin-right: 1em\'>Cancel</button>' : '') +
8034180341 '</div></div>'
8034280342 );
8034380343
80344+ const drag_move = drag().on('start', () => { modal.y0 = 0; }).on('drag', evnt => {
80345+ if (!modal.y0)
80346+ modal.y0 = pointer(evnt, modal.element.node())[1];
80347+ let p0 = Math.max(0, pointer(evnt, b.node())[1] - modal.y0);
80348+ if (b.node().clientHeight)
80349+ p0 = Math.min(p0, 0.8 * b.node().clientHeight);
80350+ modal.element.style('top', `${p0}px`);
80351+ });
80352+ modal.element.select('.jsroot_dialog_title').call(drag_move);
80353+
8034480354 modal.done = function(res) {
8034580355 if (this._done)
8034680356 return;
@@ -121180,7 +121190,10 @@ const clTStreamerElement = 'TStreamerElement', clTStreamerObject = 'TStreamerObj
121180121190 StlNames = ['', 'vector', 'list', 'deque', 'map', 'multimap', 'set', 'multiset', 'bitset'],
121181121191
121182121192 // TObject bits
121183- kIsReferenced = BIT(4), kHasUUID = BIT(5);
121193+ kIsReferenced = BIT(4), kHasUUID = BIT(5),
121194+
121195+ // gap in http which can be merged into single http request
121196+ kMinimalHttpGap = 128;
121184121197
121185121198
121186121199/** @summary Custom streamers for root classes
@@ -124173,17 +124186,77 @@ class TFile {
124173124186 * @private */
124174124187 async _open() { return this.readKeys(); }
124175124188
124189+ /** @summary check if requested segments can be reordered or merged
124190+ * @private */
124191+ #checkNeedReorder(place) {
124192+ let res = false, resort = false;
124193+ for (let n = 0; n < place.length - 2; n += 2) {
124194+ if (place[n] > place[n + 2])
124195+ res = resort = true;
124196+ if (place[n] + place[n + 1] > place[n + 2] - kMinimalHttpGap)
124197+ res = true;
124198+ }
124199+ if (!res) {
124200+ return {
124201+ place,
124202+ blobs: [],
124203+ expectedSize(indx) { return this.place[indx + 1]; },
124204+ addBuffer(indx, buf, o) {
124205+ this.blobs[indx / 2] = new DataView(buf, o, this.place[indx + 1]);
124206+ }
124207+ };
124208+ }
124209+
124210+ res = { place, reorder: [], place_new: [], blobs: [] };
124211+
124212+ for (let n = 0; n < place.length; n += 2)
124213+ res.reorder.push({ pos: place[n], len: place[n + 1], indx: [n] });
124214+
124215+ if (resort)
124216+ res.reorder.sort((a, b) => { return a.pos - b.pos; });
124217+
124218+ for (let n = 0; n < res.reorder.length - 1; n++) {
124219+ const curr = res.reorder[n],
124220+ next = res.reorder[n + 1];
124221+ if (curr.pos + curr.len + kMinimalHttpGap > next.pos) {
124222+ curr.indx.push(...next.indx);
124223+ curr.len = next.pos + next.len - curr.pos;
124224+ res.reorder.splice(n + 1, 1); // remove segment
124225+ n--;
124226+ }
124227+ }
124228+
124229+ res.reorder.forEach(elem => res.place_new.push(elem.pos, elem.len));
124230+
124231+ res.expectedSize = function(indx) {
124232+ return this.reorder[indx / 2].len;
124233+ };
124234+
124235+ res.addBuffer = function(indx, buf, o) {
124236+ const elem = this.reorder[indx / 2],
124237+ pos0 = elem.pos;
124238+ elem.indx.forEach(indx0 => {
124239+ this.blobs[indx0 / 2] = new DataView(buf, o + this.place[indx0] - pos0, this.place[indx0 + 1]);
124240+ });
124241+ };
124242+
124243+ return res;
124244+ }
124245+
124176124246 /** @summary read buffer(s) from the file
124177124247 * @return {Promise} with read buffers
124178124248 * @private */
124179124249 async readBuffer(place, filename, progress_callback) {
124180124250 if ((this.fFileContent !== null) && !filename && (!this.fAcceptRanges || this.fFileContent.canExtract(place)))
124181124251 return this.fFileContent.extract(place);
124182124252
124253+ const reorder = this.#checkNeedReorder(place);
124254+ if (reorder?.place_new)
124255+ place = reorder?.place_new;
124256+
124183124257 let resolveFunc, rejectFunc;
124184124258
124185124259 const file = this, first_block = (place[0] === 0) && (place.length === 2),
124186- blobs = [], // array of requested segments
124187124260 promise = new Promise((resolve, reject) => {
124188124261 resolveFunc = resolve;
124189124262 rejectFunc = reject;
@@ -124209,12 +124282,15 @@ class TFile {
124209124282 }
124210124283 }
124211124284
124212- function send_new_request(increment) {
124213- if (increment) {
124285+ function send_new_request(arg) {
124286+ if (arg === 'noranges') {
124287+ file.fMaxRanges = 1;
124288+ last = Math.min(last, first + file.fMaxRanges * 2);
124289+ } else if (arg) {
124214124290 first = last;
124215124291 last = Math.min(first + file.fMaxRanges * 2, place.length);
124216124292 if (first >= place.length)
124217- return resolveFunc(blobs);
124293+ return resolveFunc(reorder.blobs.length === 1 ? reorder.blobs[0] : reorder. blobs);
124218124294 }
124219124295
124220124296 let fullurl = fileurl, ranges = 'bytes', totalsz = 0;
@@ -124231,7 +124307,7 @@ class TFile {
124231124307
124232124308 // when read first block, allow to read more - maybe ranges are not supported and full file content will be returned
124233124309 if (file.fAcceptRanges && first_block)
124234- totalsz = Math.max(totalsz, 1e7 );
124310+ totalsz = Math.max(totalsz, 1e5 );
124235124311
124236124312 return createHttpRequest(fullurl, 'buf', read_callback, undefined, true).then(xhr => {
124237124313 if (file.fAcceptRanges) {
@@ -124349,70 +124425,34 @@ class TFile {
124349124425
124350124426 // if only single segment requested, return result as is
124351124427 if (last - first === 2) {
124352- const b = new DataView(res);
124353- if (place.length === 2)
124354- return resolveFunc(b);
124355- blobs.push(b);
124428+ reorder.addBuffer(first, res, 0);
124356124429 return send_new_request(true);
124357124430 }
124358124431
124359124432 // object to access response data
124360- const hdr = this.getResponseHeader('Content-Type'),
124361- ismulti = isStr(hdr) && (hdr.indexOf('multipart') >= 0),
124362- view = new DataView(res);
124363-
124364- if (!ismulti) {
124365- // server may returns simple buffer, which combines all segments together
124366-
124367- const hdr_range = this.getResponseHeader('Content-Range');
124368- let segm_start = 0, segm_last = -1;
124369-
124370- if (isStr(hdr_range) && hdr_range.indexOf('bytes') >= 0) {
124371- const parts = hdr_range.slice(hdr_range.indexOf('bytes') + 6).split(/[\s-/]+/);
124372- if (parts.length === 3) {
124373- segm_start = Number.parseInt(parts[0]);
124374- segm_last = Number.parseInt(parts[1]);
124375- if (!Number.isInteger(segm_start) || !Number.isInteger(segm_last) || (segm_start > segm_last)) {
124376- segm_start = 0;
124377- segm_last = -1;
124378- }
124379- }
124380- }
124381-
124382- let canbe_single_segment = (segm_start <= segm_last);
124383- for (let n = first; n < last; n += 2) {
124384- if ((place[n] < segm_start) || (place[n] + place[n + 1] - 1 > segm_last))
124385- canbe_single_segment = false;
124386- }
124387-
124388- if (canbe_single_segment) {
124389- for (let n = first; n < last; n += 2)
124390- blobs.push(new DataView(res, place[n] - segm_start, place[n + 1]));
124391- return send_new_request(true);
124392- }
124393-
124394- if ((file.fMaxRanges === 1) || !first)
124395- return rejectFunc(Error('Server returns normal response when multipart was requested, disable multirange support'));
124396-
124397- file.fMaxRanges = 1;
124398- last = Math.min(last, file.fMaxRanges * 2);
124433+ const hdr = this.getResponseHeader('Content-Type');
124399124434
124400- return send_new_request();
124435+ if (!isStr(hdr) || (hdr.indexOf('multipart') < 0)) {
124436+ console.error('Did not found multipart in content-type - fallback to single range request');
124437+ return send_new_request('noranges');
124401124438 }
124402124439
124403124440 // multipart messages requires special handling
124404124441
124405124442 const indx = hdr.indexOf('boundary=');
124406- let boundary = '', n = first, o = 0, normal_order = true;
124407- if (indx > 0) {
124408- boundary = hdr.slice(indx + 9);
124409- if ((boundary[0] === '"') && (boundary.at(-1) === '"'))
124410- boundary = boundary.slice(1, boundary.length - 1);
124411- boundary = '--' + boundary;
124412- } else
124413- console.error('Did not found boundary id in the response header');
124443+ if (indx <= 0) {
124444+ console.error('Did not found boundary id in the response header - fallback to single range request');
124445+ return send_new_request('noranges');
124446+ }
124447+
124448+ let boundary = hdr.slice(indx + 9);
124449+ if ((boundary[0] === '"') && (boundary.at(-1) === '"'))
124450+ boundary = boundary.slice(1, boundary.length - 1);
124451+ boundary = '--' + boundary;
124414124452
124415- while (n < last) {
124453+ const view = new DataView(res);
124454+
124455+ for (let n = first, o = 0; n < last; n += 2) {
124416124456 let code1, code2 = view.getUint8(o), nline = 0, line = '',
124417124457 finish_header = false, segm_start = 0, segm_last = -1;
124418124458
@@ -124431,6 +124471,7 @@ class TFile {
124431124471 if (parts.length === 3) {
124432124472 segm_start = Number.parseInt(parts[0]);
124433124473 segm_last = Number.parseInt(parts[1]);
124474+ // TODO: check for consistency
124434124475 if (!Number.isInteger(segm_start) || !Number.isInteger(segm_last) || (segm_start > segm_last)) {
124435124476 segm_start = 0;
124436124477 segm_last = -1;
@@ -124453,44 +124494,16 @@ class TFile {
124453124494 o++;
124454124495 }
124455124496
124456- if (!finish_header)
124457- return rejectFunc(Error('Cannot decode header in multipart message'));
124458-
124459- if (segm_start > segm_last) {
124460- // fall-back solution, believe that segments same as requested
124461- blobs.push(new DataView(res, o, place[n + 1]));
124462- o += place[n + 1];
124463- n += 2;
124464- } else if (normal_order) {
124465- const n0 = n;
124466- while ((n < last) && (place[n] >= segm_start) && (place[n] + place[n + 1] - 1 <= segm_last)) {
124467- blobs.push(new DataView(res, o + place[n] - segm_start, place[n + 1]));
124468- n += 2;
124469- }
124497+ const segm_size = segm_last - segm_start + 1;
124470124498
124471- if (n > n0)
124472- o += (segm_last - segm_start + 1);
124473- else
124474- normal_order = false;
124499+ if (!finish_header || (segm_size <= 0) || (reorder.expectedSize(n) !== segm_size)) {
124500+ console.error('Failure decoding multirange header - fallback to single range request');
124501+ return send_new_request('noranges');
124475124502 }
124476124503
124477- if (!normal_order) {
124478- // special situation when server reorder segments in the reply
124479- let isany = false;
124480- for (let n1 = n; n1 < last; n1 += 2) {
124481- if ((place[n1] >= segm_start) && (place[n1] + place[n1 + 1] - 1 <= segm_last)) {
124482- blobs[n1 / 2] = new DataView(res, o + place[n1] - segm_start, place[n1 + 1]);
124483- isany = true;
124484- }
124485- }
124486- if (!isany)
124487- return rejectFunc(Error(`Provided fragment ${segm_start} - ${segm_last} out of requested multi-range request`));
124488-
124489- while (blobs[n / 2])
124490- n += 2;
124504+ reorder.addBuffer(n, res, o);
124491124505
124492- o += (segm_last - segm_start + 1);
124493- }
124506+ o += segm_size;
124494124507 }
124495124508
124496124509 send_new_request(true);
0 commit comments