querysub 0.349.0 → 0.351.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.349.0",
3
+ "version": "0.351.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -32,7 +32,6 @@
32
32
  "gc-watch-public": "./bin/gc-watch-public.js",
33
33
  "join": "./bin/join.js",
34
34
  "join-public": "./bin/join-public.js",
35
- "merge": "./bin/merge.js",
36
35
  "addsuperuser": "./bin/addsuperuser.js",
37
36
  "error-email": "./bin/error-email.js",
38
37
  "error-im": "./bin/error-im.js"
@@ -51,9 +50,9 @@
51
50
  "js-sha512": "^0.9.0",
52
51
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
53
52
  "pako": "^2.1.0",
54
- "socket-function": "^0.153.0",
53
+ "socket-function": "^1.0.1",
55
54
  "terser": "^5.31.0",
56
- "typesafecss": "^0.23.0",
55
+ "typesafecss": "^0.25.0",
57
56
  "yaml": "^2.5.0",
58
57
  "yargs": "^15.3.1"
59
58
  },
@@ -550,6 +550,8 @@ export class ArchivesBackblaze {
550
550
  // we can remove this line.
551
551
  || err.stack.includes(`400 Bad Request`)
552
552
  || err.stack.includes(`getaddrinfo ENOTFOUND`)
553
+ || err.stack.includes(`ECONNRESET`)
554
+ || err.stack.includes(`ECONNREFUSED`)
553
555
  ) {
554
556
  this.log(err.message + " retrying in 5s");
555
557
  await delay(5000);
@@ -1015,7 +1015,7 @@ export class PathValueProxyWatcher {
1015
1015
  options: WatcherOptions<Result>
1016
1016
  ): SyncWatcher {
1017
1017
  if (isAsyncFunction(options.watchFunction)) {
1018
- throw new Error(`Async functions are not supported in watchers. They must run the caller synchronously. You are likely not using Await anyway, so just remove the async and make it a synchronous function. The caller will be called again whenever the data you access changes, And if you are running this to return a result, it will be rerun until all the data you want is synchronized. Watch function: ${options.watchFunction.toString()}`);
1018
+ throw new Error(`A watcher function cannot be async, it must be synchronous. You probably did Querysub.commitAsync(async () => {}). Just remove the async, and do Querysub.commit(() => {}). The caller will be called again whenever the data you access changes, And if you are running this to return a result, it will be rerun until all the data you want is synchronized. Watch function: ${options.watchFunction.toString()}`);
1019
1019
  }
1020
1020
  // NOTE: Setting an order is needed for rendering, so parents render before children. I believe
1021
1021
  // it is generally what we want, so event triggering is consistent, and fits with any tree based
@@ -74,6 +74,7 @@ function closeModal(id: string) {
74
74
  export function showModal(config: {
75
75
  content: preact.ComponentChild;
76
76
  onClose?: () => void | "abortClose";
77
+ onlyCloseExplicitly?: boolean;
77
78
  }): {
78
79
  close: () => void;
79
80
  } {
@@ -83,6 +84,7 @@ export function showModal(config: {
83
84
  data().modals[id] = atomicObjectWriteNoFreeze({
84
85
  value: config.content,
85
86
  onClose: config.onClose,
87
+ onlyExplicitClose: config.onlyCloseExplicitly,
86
88
  });
87
89
  });
88
90
  function close() {
@@ -94,6 +96,7 @@ export function showModal(config: {
94
96
  export function closeAllModals() {
95
97
  Querysub.commit(() => {
96
98
  for (let [id, obj] of Object.entries(data().modals)) {
99
+ if (obj.onlyExplicitClose) continue;
97
100
  closeModal(id);
98
101
  }
99
102
  });
@@ -35,6 +35,7 @@ import { assertIsNetworkTrusted } from "../../-d-trust/NetworkTrust2";
35
35
  import { blue, magenta } from "socket-function/src/formatting/logColors";
36
36
  import { FileMetadata, FastArchiveAppendableControllerBase, FastArchiveAppendableController, getFileMetadataHash } from "./FastArchiveController";
37
37
  import { fsExistsAsync } from "../../fs";
38
+ import { ScanFnc } from "./FastArchiveViewer";
38
39
 
39
40
  // NOTE: In a single command line micro-test it looks like we can write about 40K writes of 500 per once, when using 10X parallel, on a fairly potato server. We should probably batch though, and only do 1X parallel.
40
41
  /*
@@ -360,6 +361,7 @@ export class FastArchiveAppendable<Datum> {
360
361
  endTime: number;
361
362
  };
362
363
  cacheBust: number;
364
+ scanFnc?: ScanFnc;
363
365
  getWantData?: (file: FileMetadata) => Promise<((posStart: number, posEnd: number, data: Buffer) => boolean) | undefined>;
364
366
  onData: (datum: Datum[], file: FileMetadata) => void;
365
367
  // Called after onData
@@ -519,7 +521,7 @@ export class FastArchiveAppendable<Datum> {
519
521
  let processProgress = await createProgress("Processing (datums)", 0);
520
522
  let corruptDatumsProgress = await createProgress("Corrupt Datums (datums)", 0);
521
523
 
522
- const self = this;
524
+ let scanFnc = config.scanFnc;
523
525
 
524
526
 
525
527
  async function downloadAndParseFile(file: FileMetadata, runInner: (code: () => Promise<void>) => Promise<void>) {
@@ -546,6 +548,9 @@ export class FastArchiveAppendable<Datum> {
546
548
  scanProgressCount++;
547
549
  }
548
550
  if (buffer !== "done") {
551
+ if (scanFnc) {
552
+ scanFnc(posStart, posEnd, buffer);
553
+ }
549
554
  if (wantData && !wantData(posStart, posEnd, buffer)) {
550
555
  notMatchedSize += (posEnd - posStart);
551
556
  notMatchedCount++;
@@ -25,15 +25,22 @@ const RENDER_INTERVAL = 1000;
25
25
  const HISTOGRAM_RERENDER_INTERVAL = 10000;
26
26
 
27
27
  export const filterParam = new URLParam("filter", "");
28
+ // If filter2 is set, we also need it. Not great (why not support arbitrary counts of these), but for now... this should be fine (and support arbitrary counts might slow down our incredibly optimized scan function, where most of our time is spent).
29
+ // NOTE: This supports less than filterParam (no object parsing, it only does text contains)
30
+ export const filterParam2 = new URLParam("filter2", "");
31
+
28
32
  export const cacheBustParam = new URLParam("cacheBust", 0);
29
33
  const caseInsensitiveParam = new URLParam("caseInsensitive", false);
30
34
  const hideAllDataParam = new URLParam("hideAllData", false);
31
35
 
36
+ export type ScanFnc = (posStart: number, posEnd: number, data: Buffer) => boolean;
37
+
32
38
  export class FastArchiveViewer<T> extends qreact.Component<{
33
39
  fastArchives: FastArchiveAppendable<T>[];
34
40
  runOnLoad?: boolean;
35
41
  onStart: () => MaybePromise<void>;
36
- getWantData?: (file: FileMetadata) => Promise<((posStart: number, posEnd: number, data: Buffer) => boolean) | undefined>;
42
+ getScanFnc?: () => ScanFnc;
43
+ getWantData?: (file: FileMetadata) => Promise<ScanFnc | undefined>;
37
44
  onDatums: (source: FastArchiveAppendable<T>, datums: T[], metadata: FileMetadata) => void;
38
45
  // Called after onData
39
46
  onStats?: (source: FastArchiveAppendable<T>, stats: DatumStats, metadata: FileMetadata) => void;
@@ -108,6 +115,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
108
115
 
109
116
  @measureFnc
110
117
  private async synchronizeData() {
118
+ console.log("synchronizeData");
111
119
  let onStart = Querysub.fastRead(() => this.props.onStart);
112
120
  let onDatums = Querysub.fastRead(() => this.props.onDatums);
113
121
  let onStats = Querysub.fastRead(() => this.props.onStats);
@@ -144,7 +152,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
144
152
 
145
153
  const timeRange = getTimeRange();
146
154
  let filterString = filterParam.value;
147
-
155
+ let filterString2 = filterParam2.value;
148
156
  let scannedValueCount = 0;
149
157
 
150
158
  // Store current sync parameters BEFORE calling synchronizeData
@@ -310,6 +318,15 @@ export class FastArchiveViewer<T> extends qreact.Component<{
310
318
  });
311
319
  let { scanMatch, datumMatch } = joinMatches(andMatches, "||");
312
320
 
321
+ let andMatches2 = filterString2.split("|").map(orParts => {
322
+ let andParts = orParts.split("&");
323
+ let andMatches = andParts.map(part => parsePart(part.trim()));
324
+ return joinMatches(andMatches, "&&");
325
+ });
326
+ let scanObj2 = joinMatches(andMatches2, "||");
327
+ let scanMatch2 = scanObj2.scanMatch;
328
+ let datumMatch2 = scanObj2.datumMatch;
329
+ let useScan2 = !!filterString2.trim();
313
330
 
314
331
  const limitedBuffer = Buffer.from(LOG_LIMIT_FLAG);
315
332
 
@@ -318,6 +335,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
318
335
  const result = await fastArchive.synchronizeData({
319
336
  range: timeRange,
320
337
  cacheBust: Querysub.fastRead(() => cacheBustParam.value),
338
+ scanFnc: Querysub.fastRead(() => this.props.getScanFnc?.()),
321
339
  getWantData: async (file) => {
322
340
  let wantData = await getWantData?.(file);
323
341
  return (posStart: number, posEnd: number, data: Buffer) => {
@@ -341,6 +359,9 @@ export class FastArchiveViewer<T> extends qreact.Component<{
341
359
 
342
360
  // scanMatch is faster than wantData (generally), so run it first
343
361
  let matched = scanMatch(posStart, posEnd, data);
362
+ if (matched && useScan2) {
363
+ matched = scanMatch2(posStart, posEnd, data);
364
+ }
344
365
  if (matched && wantData) {
345
366
  matched = wantData(posStart, posEnd, data);
346
367
  }
@@ -635,8 +656,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
635
656
  return warnings;
636
657
  }
637
658
 
638
- public handleDownload = throttleFunction(500, () => {
639
- console.log("handleDownload");
659
+ public synchronizeDataThrottled = throttleFunction(500, () => {
640
660
  Querysub.onCommitFinished(() => {
641
661
  logErrors(this.synchronizeData());
642
662
  });
@@ -683,7 +703,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
683
703
  label={
684
704
  <div className={css.hbox(10)}>
685
705
  <Button onClick={() => {
686
- void this.handleDownload();
706
+ void this.synchronizeDataThrottled();
687
707
  }}>
688
708
  Run
689
709
  </Button>
@@ -694,10 +714,10 @@ export class FastArchiveViewer<T> extends qreact.Component<{
694
714
  hot
695
715
  flavor="large"
696
716
  fillWidth
697
- onKeyUp={this.handleDownload}
717
+ onKeyUp={this.synchronizeDataThrottled}
698
718
  ref2={() => {
699
719
  if (this.props.runOnLoad) {
700
- void this.handleDownload();
720
+ void this.synchronizeDataThrottled();
701
721
  }
702
722
  }}
703
723
  noEnterKeyBlur
@@ -708,7 +728,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
708
728
  checkbox
709
729
  url={caseInsensitiveParam}
710
730
  onChange={() => {
711
- void this.handleDownload();
731
+ void this.synchronizeDataThrottled();
712
732
  }}
713
733
  flavor="small"
714
734
  />
@@ -729,7 +749,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
729
749
  <div
730
750
  className={infoDisplay(30).button}
731
751
  onClick={() => {
732
- void this.handleDownload();
752
+ void this.synchronizeDataThrottled();
733
753
  }}
734
754
  title={outdatedWarnings.join(", ")}
735
755
  >
@@ -746,7 +766,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
746
766
  })()}
747
767
  {this.state.runCount === 0 && (
748
768
  <div className={infoDisplay(200).button} onClick={() => {
749
- void this.handleDownload();
769
+ void this.synchronizeDataThrottled();
750
770
  }}>
751
771
  No data downloaded yet. Click here to download data.
752
772
  </div>
@@ -766,7 +786,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
766
786
  Querysub.commit(() => {
767
787
  cacheBustParam.value = Date.now();
768
788
  });
769
- void this.handleDownload();
789
+ void this.synchronizeDataThrottled();
770
790
  }}
771
791
  >
772
792
  <div className={css.hbox(8).alignItems("center").wrap}>
@@ -783,7 +803,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
783
803
  <div className={infoDisplay(60).boldStyle.button}
784
804
  onClick={() => {
785
805
  filterParam.value = LOG_LIMIT_FLAG;
786
- void this.handleDownload();
806
+ void this.synchronizeDataThrottled();
787
807
  }}
788
808
  >
789
809
  Click here to see {formatNumber(this.limitedScanCount)} scanned logs were rate limited at a file level. This means lines might be missing. If this happens a lot, use pass {`{ [LOG_LINE_LIMIT_ID]: "INSERT RANDOM GUID HERE" }`} in the error/warn message to limit only the spamming line.
@@ -793,7 +813,7 @@ export class FastArchiveViewer<T> extends qreact.Component<{
793
813
  <div className={infoDisplay(0).boldStyle.button}
794
814
  onClick={() => {
795
815
  filterParam.value += " & " + LOG_LIMIT_FLAG;
796
- void this.handleDownload();
816
+ void this.synchronizeDataThrottled();
797
817
  }}
798
818
  >
799
819
  Click here to see {formatNumber(this.limitedMatchCount)} matched logs were rate limited at a file level. This means lines might be missing. If this happens a lot, use pass {`{ [LOG_LINE_LIMIT_ID]: "INSERT RANDOM GUID HERE" }`} in the error/warn message to limit only the spamming line.
@@ -13,7 +13,7 @@ import { logErrors } from "../../errors";
13
13
  import { batchFunction, runInSerial } from "socket-function/src/batching";
14
14
  import { Querysub } from "../../4-querysub/QuerysubController";
15
15
  import { sort, timeInDay, timeInHour } from "socket-function/src/misc";
16
- import { FastArchiveViewer, cacheBustParam, filterParam } from "./FastArchiveViewer";
16
+ import { FastArchiveViewer, cacheBustParam, filterParam, filterParam2 } from "./FastArchiveViewer";
17
17
  import { LogDatum, getLoggers, LOG_LIMIT_FLAG, LOG_LINE_LIMIT_FLAG } from "./diskLogger";
18
18
  import { ColumnType, Table, TableType } from "../../5-diagnostics/Table";
19
19
  import { formatDateJSX } from "../../misc/formatJSX";
@@ -28,6 +28,7 @@ import { RecentErrorsController, SuppressionListController, getSuppressEntryChec
28
28
  import { SocketFunction } from "socket-function/SocketFunction";
29
29
  import { throttleRender } from "../../functional/throttleRender";
30
30
  import { isNode } from "typesafecss";
31
+ import { createLogViewerExtractField } from "./logViewerExtractField";
31
32
 
32
33
  const RENDER_INTERVAL = 1000;
33
34
 
@@ -56,6 +57,7 @@ const defaultSelectedFields = {
56
57
  export class LogViewer2 extends qreact.Component {
57
58
  state = t.state({
58
59
  datumsSeqNum: t.atomic<number>(0),
60
+ showCountByFile: t.boolean,
59
61
  });
60
62
 
61
63
  private example: string | undefined = undefined;
@@ -66,6 +68,7 @@ export class LogViewer2 extends qreact.Component {
66
68
  private errors = 0;
67
69
  private notMatchedCount = 0;
68
70
  private datums: LogDatum[] = [];
71
+ private countsPerName = new Map<string, number>();
69
72
 
70
73
  private lastRenderTime = 0;
71
74
  private suppressionCounts = new Map<string, number>();
@@ -74,7 +77,7 @@ export class LogViewer2 extends qreact.Component {
74
77
 
75
78
  rerun() {
76
79
  cacheBustParam.value = Date.now();
77
- void this.fastArchiveViewer?.handleDownload();
80
+ void this.fastArchiveViewer?.synchronizeDataThrottled();
78
81
  }
79
82
 
80
83
  render() {
@@ -166,6 +169,7 @@ export class LogViewer2 extends qreact.Component {
166
169
  this.datums = [];
167
170
  this.suppressionCounts = new Map();
168
171
  this.expiredSuppressionCounts = new Map();
172
+ this.countsPerName = new Map();
169
173
  this.operationSequenceNum++;
170
174
  void (async () => {
171
175
  const currentSequenceNum = this.operationSequenceNum;
@@ -183,10 +187,21 @@ export class LogViewer2 extends qreact.Component {
183
187
  suppressionList = await suppressionController.getSuppressionList.promise();
184
188
 
185
189
  }}
190
+ getScanFnc={() => {
191
+ if (!this.state.showCountByFile) {
192
+ return () => false;
193
+ }
194
+ return createLogViewerExtractField("name", (value) => {
195
+ let prevCount = this.countsPerName.get(value) ?? 0;
196
+ prevCount++;
197
+ this.countsPerName.set(value, prevCount);
198
+ });
199
+ }}
186
200
  getWantData={async (file) => {
187
201
  if (!hasErrorNotifyToggle) return undefined;
188
202
  // By defaulting to the synchronous one, the list will be updated if there's any changes. However, we will also just get it asynchronously if the list hasn't been updated by the time we call get1data. And because we assign it back to the variable, it'll be cached.
189
203
  suppressionList = suppressionList || await suppressionController.getSuppressionList.promise();
204
+
190
205
  let suppressionFull = getSuppressionFull({
191
206
  entries: suppressionList,
192
207
  blockTimeRange: {
@@ -329,6 +344,7 @@ export class LogViewer2 extends qreact.Component {
329
344
  </div>;
330
345
  }
331
346
  };
347
+ let includedFiles = filterParam2.value.split("|").map(x => x.trim());
332
348
  return <>
333
349
  <div className={css.hbox(10)}>
334
350
  <InputPicker
@@ -373,6 +389,79 @@ export class LogViewer2 extends qreact.Component {
373
389
  {this.datums.length === 0 && <div className={css.hsl(40, 50, 50).colorhsl(60, 50, 100).boldStyle.pad2(10).ellipsis}>
374
390
  No logs matched, either increase the time range or decrease the filter specificity.
375
391
  </div>}
392
+ {includedFiles.length > 0 && includedFiles[0] && <div className={css.hbox(20)}>
393
+ <div className={css.hbox(10)}>
394
+ <div className={css.fontSize(18).hsl(0, 50, 50).colorhsl(0, 50, 95).pad2(8, 4)}>
395
+ Only showing {includedFiles.length} files:
396
+ </div>
397
+ <Button onClick={() => {
398
+ filterParam2.value = "";
399
+ this.rerun();
400
+ }}>
401
+ Clear All Filters
402
+ </Button>
403
+ </div>
404
+ <div className={css.hbox(10, 10).wrap}>
405
+ {includedFiles.filter(x => x).map((fileName) => (
406
+ <div
407
+ key={fileName}
408
+ className={css.pad2(8, 4).hsl(120, 40, 80).bord2(120, 40, 60, 1).button}
409
+ onClick={() => {
410
+ let newFiles = includedFiles.filter(x => x !== fileName);
411
+ filterParam2.value = newFiles.join("|");
412
+ }}
413
+ title={`Click to remove ${fileName} from filter`}
414
+ >
415
+ {fileName} ✕
416
+ </div>
417
+ ))}
418
+ </div>
419
+ </div>}
420
+
421
+ {this.state.showCountByFile && (() => {
422
+ let counts = Array.from(this.countsPerName.entries());
423
+ sort(counts, x => -x[1]);
424
+ let topCounts = counts.slice(0, 20);
425
+ let totalCount = counts.reduce((sum, [, count]) => sum + count, 0);
426
+
427
+ return <div className={css.vbox(10)}>
428
+ <div className={css.hbox(10, 10).wrap}>
429
+ {topCounts.map(([name, count]) => {
430
+ let percentage = totalCount > 0 ? (count / totalCount) * 100 : 0;
431
+ let isFiltered = includedFiles.includes(name);
432
+
433
+ return <div
434
+ key={name}
435
+ className={css.relative.pad2(8, 4).bord2(210, 30, 60, 1).ellipsis.button}
436
+ title={isFiltered ? `Click to remove ${name} from filter` : `Click to add ${name} to filter`}
437
+ onClick={() => {
438
+ if (isFiltered) {
439
+ // Remove from filter
440
+ let newFiles = includedFiles.filter(x => x !== name);
441
+ filterParam2.value = newFiles.join("|");
442
+ } else {
443
+ // Add to filter
444
+ let newFiles = [...includedFiles.filter(x => x), name];
445
+ filterParam2.value = newFiles.join("|");
446
+ }
447
+ }}
448
+ >
449
+ <div
450
+ className={css.absolute.pos(0, 0).height("100%").width(`${percentage}%`).hsl(isFiltered ? 120 : 210, 40, 80)}
451
+ />
452
+ <div className={css.relative.maxWidth(200).ellipsis}>
453
+ {formatNumber(count)} | {name || "(no name present)"} {isFiltered && "✓" || ""}
454
+ </div>
455
+ </div>;
456
+ })}
457
+ </div>
458
+ </div>;
459
+ })() || <Button onClick={() => {
460
+ this.state.showCountByFile = true;
461
+ this.rerun();
462
+ }}>
463
+ Show Count by File
464
+ </Button>}
376
465
  <Table
377
466
  rows={this.datums}
378
467
  columns={columns}
@@ -548,14 +548,16 @@ export class RecentErrors {
548
548
  });
549
549
 
550
550
  private _recentErrors: LogDatum[] = [];
551
- private updateRecentErrors = runInSerial(async (objs: LogDatum[]) => {
552
- objs = await suppressionList.filterObjsToNonSuppressed(objs);
553
- let newRecentErrors = limitRecentErrors(objs);
551
+ private _lastSentErrors: LogDatum[] = [];
552
+ private updateRecentErrors = runInSerial(async () => {
553
+ let newList = this._recentErrors;
554
+ newList = await suppressionList.filterObjsToNonSuppressed(newList);
555
+ newList = limitRecentErrors(newList);
554
556
  // If any changed
555
- let prev = new Set(this._recentErrors);
556
- let newErrors = new Set(newRecentErrors);
557
+ let prev = new Set(this._lastSentErrors);
558
+ let newErrors = new Set(newList);
557
559
  function hasAnyChanged() {
558
- for (let obj of newRecentErrors) {
560
+ for (let obj of newList) {
559
561
  if (!prev.has(obj)) {
560
562
  return true;
561
563
  }
@@ -567,8 +569,8 @@ export class RecentErrors {
567
569
  }
568
570
  return false;
569
571
  }
572
+ this._lastSentErrors = newList;
570
573
  if (hasAnyChanged()) {
571
- this._recentErrors = newRecentErrors;
572
574
  void this.broadcastUpdate(undefined);
573
575
  }
574
576
  });
@@ -586,7 +588,7 @@ export class RecentErrors {
586
588
  for (let obj of objs) {
587
589
  this._recentErrors.push(obj);
588
590
  }
589
- await this.updateRecentErrors(this._recentErrors);
591
+ await this.updateRecentErrors();
590
592
  });
591
593
 
592
594
  private lastSuppressionList = new Map<string, SuppressionEntry>();
@@ -613,7 +615,7 @@ export class RecentErrors {
613
615
  void this.scanNow({});
614
616
  }
615
617
  this.lastSuppressionList = newSuppressionList;
616
- await this.updateRecentErrors(this._recentErrors);
618
+ await this.updateRecentErrors();
617
619
  });
618
620
 
619
621
  private scannedHashes = new Set<string>();
@@ -636,7 +638,7 @@ export class RecentErrors {
636
638
  noLocalFiles: config.noLocalFiles,
637
639
  });
638
640
  // Filter again, as new suppressions change the errors
639
- await this.updateRecentErrors(this._recentErrors);
641
+ await this.updateRecentErrors();
640
642
  let recentLimit = 0;
641
643
  const applyRecentLimit = () => {
642
644
  if (this._recentErrors.length < MAX_RECENT_ERRORS) return;
@@ -147,7 +147,9 @@ export class ErrorWarning extends qreact.Component {
147
147
  if (recentErrors[0].__matchedOutdatedSuppressionKey) {
148
148
  let key = recentErrors[0].__matchedOutdatedSuppressionKey;
149
149
  topExpired = suppressionList.find(x => x.key === key);
150
-
150
+ }
151
+ function showSpecialCharacters(text: string) {
152
+ return text.replaceAll("\n", "\\n");
151
153
  }
152
154
 
153
155
  return (
@@ -181,12 +183,12 @@ export class ErrorWarning extends qreact.Component {
181
183
  Match Pattern =
182
184
  </div>
183
185
  <div className={css.hsl(200, 40, 40).pad2(4, 2).colorhsl(0, 0, 95)}>
184
- {topExpired.match}
186
+ {showSpecialCharacters(topExpired.match)}
185
187
  </div>
186
188
  </div>
187
189
  }
188
190
  <div className={css.hbox(8).hsl(0, 50, 50).pad2(4, 2).colorhsl(0, 50, 95)}>
189
- ({formatDateJSX(recentErrors[0].time)}) {recentErrors[0].param0} ({recentErrors[0].__NAME__})
191
+ ({formatDateJSX(recentErrors[0].time)}) {showSpecialCharacters(String(recentErrors[0].param0))} ({recentErrors[0].__NAME__})
190
192
  </div>
191
193
 
192
194
  <div className={css.hbox(8).fillWidth}>
@@ -121,7 +121,12 @@ const sendIMs = batchFunction(({ delay: BATCH_TIME }), async (logsAll: LogDatum[
121
121
  return;
122
122
  }
123
123
  let url = createLink(getErrorLogsLink());
124
- let message = Object.values(info.perFile).flat().map(
124
+ let flat = Object.values(info.perFile).flat();
125
+ if (flat.length === 0) {
126
+ console.log(`All messages have hit their IM limit, so not sending any message at all.`);
127
+ return;
128
+ }
129
+ let message = flat.map(
125
130
  x => `[${formatDateTime(x.time)}](${url}) | ${ellipsisMiddle(String(x.param0), 100)} (${x.__NAME__})`
126
131
  ).join("\n");
127
132
  message = ellipsisMiddle(message, 900);
@@ -0,0 +1,37 @@
1
+ import { ScanFnc } from "./FastArchiveViewer";
2
+
3
+ export function createLogViewerExtractField(
4
+ fieldName: string,
5
+ onFieldValue: (value: string) => void,
6
+ ): ScanFnc {
7
+ let fieldNameBuffer = Buffer.from(`${JSON.stringify(fieldName)}:"`);
8
+ let quoteChar = Buffer.from(`"`)[0];
9
+ let escapeChar = Buffer.from(`\\`)[0];
10
+ return (posStart: number, posEnd: number, data: Buffer) => {
11
+ for (let i = posStart; i < posEnd; i++) {
12
+ if (data[i] === fieldNameBuffer[0]) {
13
+ for (let j = 1; j < fieldNameBuffer.length; j++) {
14
+ if (data[i + j] !== fieldNameBuffer[j]) {
15
+ break;
16
+ }
17
+ if (j === fieldNameBuffer.length - 1) {
18
+ // Parse until the string ends, ignoring escaped characters
19
+ let start = i + j + 1;
20
+ for (let k = start; k < posEnd; k++) {
21
+ if (data[k] === quoteChar) {
22
+ onFieldValue(data.slice(start, k).toString());
23
+ break;
24
+ }
25
+ if (data[k] === escapeChar) {
26
+ k++;
27
+ }
28
+ }
29
+ return true;
30
+ }
31
+ }
32
+ }
33
+ }
34
+ onFieldValue("");
35
+ return false;
36
+ };
37
+ }
@@ -35,7 +35,9 @@ export class InputPicker<T> extends qreact.Component<{
35
35
  state = {
36
36
  pendingText: "",
37
37
  focused: false,
38
+ showLimit: this.props.matchLimit ?? 10,
38
39
  };
40
+ inputElem: HTMLInputElement | null = null;
39
41
  render() {
40
42
  const { singleOption, fillWidth } = this.props;
41
43
  // Input, and beside it the picked values
@@ -63,7 +65,7 @@ export class InputPicker<T> extends qreact.Component<{
63
65
  pendingMatches = resolvedOptions;
64
66
  }
65
67
  let extra = pendingMatches.length;
66
- pendingMatches = pendingMatches.slice(0, this.props.matchLimit ?? 10);
68
+ pendingMatches = pendingMatches.slice(0, this.state.showLimit);
67
69
  extra -= pendingMatches.length;
68
70
  return (
69
71
  <div class={
@@ -78,6 +80,7 @@ export class InputPicker<T> extends qreact.Component<{
78
80
  {this.props.label}
79
81
  </div>
80
82
  <Input
83
+ inputRef={x => this.inputElem = x}
81
84
  value={this.state.pendingText}
82
85
  hot
83
86
  alwaysUseLatestValueWhenFocused
@@ -119,7 +122,17 @@ export class InputPicker<T> extends qreact.Component<{
119
122
  </Button>
120
123
  ))}
121
124
  {extra > 0 && (
122
- <Button class={css.hbox(5).button} disabled>
125
+ <Button class={css.hbox(5).button + " keepModalsOpen"}
126
+ onMouseDown={e => {
127
+ this.state.showLimit *= 2;
128
+ // HACK: Trying to prevent the event from causing a blur doesn't seem to work, so we'll just refocus it. And also waiting for 0 milliseconds doesn't work, so we'll wait for 100 milliseconds.
129
+ // - If the user clicks too fast this doesn't work, but the on-mouse out handler also won't work, Because we'll blur before on mouse up and then will be removed.
130
+ // TODO: Instead of keeping track of the focus state, we should make it a palette and when the palette goes away we should hide the open state.
131
+ setTimeout(() => {
132
+ this.inputElem?.focus();
133
+ }, 1000);
134
+ }}
135
+ >
123
136
  + {extra} more...
124
137
  </Button>
125
138
  )}
@@ -85,7 +85,7 @@ class CloseWrapper extends qreact.Component<{ seqNum: number }> {
85
85
  }
86
86
  }
87
87
  let ensureNotifications = lazy(() => {
88
- showModal({ content: <Notifications /> });
88
+ showModal({ content: <Notifications />, onlyCloseExplicitly: true });
89
89
  });
90
90
 
91
91
  /** Allows capturing the current state so the notification can be closed later on */
@@ -52,9 +52,11 @@ function onMessage(config: {
52
52
  // Detach entirely from any state, as we are triggered from console.error, which may be called in
53
53
  // very fragile states.
54
54
  setImmediate(() => {
55
- let title = stripConsoleRenderedColors(config.message).split("\n")[0];
55
+ let title = stripConsoleRenderedColors(config.message);
56
56
  if (title.startsWith("Error: ")) {
57
- title = title.slice("Error: ".length);
57
+ title = title.slice("Error: ".length).split("\n")[0];
58
+ } else {
59
+ title = title.replaceAll("\n", " ").slice(0, 200);
58
60
  }
59
61
  showNotification({
60
62
  pos: "topRight",
@@ -80,5 +82,5 @@ function onMessage(config: {
80
82
  // \033[38;2;<r>;<g>;<b>m
81
83
 
82
84
  function stripConsoleRenderedColors(text: string) {
83
- return String(text).replaceAll(/\033\[\d+(;\d+)*m/g, "");
84
- }
85
+ return String(text).replaceAll(/\x1b\[\d+(;\d+)*m/g, "");
86
+ }