home-hosted 0.7.15 → 0.7.17

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/dist/cli.js CHANGED
@@ -346,14 +346,95 @@ function withoutTrailingBlank(lines) {
346
346
  *
347
347
  * The current file plus its `.1` ancestor, so `logs` shows what happened just before a
348
348
  * rotation rather than starting blank. `lines <= 0` means the whole thing.
349
+ *
350
+ * Reads only the tail it needs. The log is capped at 5 MB per file *by rotation*, so a panel
351
+ * that has been up a while keeps a 5 MB current file plus a 5 MB `.1` — and the old version
352
+ * read both in full, split every line and threw all but the last fifty away: 24 ms and 28 MB
353
+ * of heap to print fifty lines, on the *default* invocation of a diagnostic command. Most of
354
+ * a tail is not looked at, so it is not read.
349
355
  */
350
356
  function readLog(file, lines) {
357
+ if (lines <= 0) return withoutTrailingBlank(readBoth(file).join("").split("\n"));
358
+ const tail = tailText(file, lines);
359
+ if (tail === null) return withoutTrailingBlank(readBoth(file).join("").split("\n")).slice(-lines);
360
+ return withoutTrailingBlank(tail.split("\n")).slice(-lines);
361
+ }
362
+ /** Both sides of the rotation, oldest first. Missing files contribute nothing. */
363
+ function readBoth(file) {
351
364
  const parts = [];
352
365
  for (const candidate of [`${file}.1`, file]) try {
353
366
  parts.push(fs.readFileSync(candidate, "utf8"));
354
367
  } catch {}
355
- const all = withoutTrailingBlank(parts.join("").split("\n"));
356
- return lines > 0 ? all.slice(-lines) : all;
368
+ return parts;
369
+ }
370
+ /**
371
+ * The text of the last `lines` lines across the rotation, joined the way concatenating the
372
+ * two files would join it — or `null` when the tail cannot be reached within the block budget,
373
+ * in which case the caller reads everything.
374
+ *
375
+ * Concatenating `.1` and the current file makes `.1`'s unterminated last line and the current
376
+ * file's first line *one* line. Keeping the two tails as text and joining them reproduces that
377
+ * for free: no gluing to arrange, and no line to lose. (An earlier draft spliced the arrays
378
+ * instead and dropped that line.)
379
+ */
380
+ function tailText(file, lines) {
381
+ const wanted = lines + 1;
382
+ const current = readTail(file, wanted);
383
+ if (current === null) return null;
384
+ if (countNewlines(current) >= wanted) return current;
385
+ const rotated = readTail(`${file}.1`, wanted);
386
+ if (rotated === null) return null;
387
+ return `${rotated}${current}`;
388
+ }
389
+ /**
390
+ * The last `lines` lines of one file, as text — read backwards in blocks, stopping as soon as
391
+ * enough line separators have been seen. `''` when there is nothing to contribute, `null` when
392
+ * the file cannot be read at all (which the caller answers by reading everything).
393
+ *
394
+ * The first element may be a fragment: a block boundary lands mid-line. It is deliberately
395
+ * left in place. The loop only exits once `lines + 1` separators are present, so at least
396
+ * `lines` complete lines follow the fragment and the caller's `slice(-lines)` can never reach
397
+ * it — trimming it would be code no test could make fail, and a randomised comparison against
398
+ * the previous implementation confirmed removing it changes nothing.
399
+ */
400
+ function readTail(file, lines) {
401
+ let handle;
402
+ try {
403
+ handle = fs.openSync(file, "r");
404
+ } catch {
405
+ return "";
406
+ }
407
+ try {
408
+ const size = fs.fstatSync(handle).size;
409
+ if (size === 0) return "";
410
+ const block = 65536;
411
+ let end = size;
412
+ let text = "";
413
+ while (end > 0 && countNewlines(text) < lines + 1) {
414
+ const start = Math.max(0, end - block);
415
+ const length = end - start;
416
+ const buffer = Buffer$1.alloc(length);
417
+ fs.readSync(handle, buffer, 0, length, start);
418
+ text = buffer.toString("utf8") + text;
419
+ end = start;
420
+ }
421
+ return text;
422
+ } catch {
423
+ return null;
424
+ } finally {
425
+ fs.closeSync(handle);
426
+ }
427
+ }
428
+ /**
429
+ * The last `lines` lines of one file, without reading the whole of it.
430
+ *
431
+ * Reads backwards in blocks and stops as soon as it has enough newlines. A single line can
432
+ * still be long, so the block grows rather than the read being capped at the exact byte.
433
+ */
434
+ function countNewlines(text) {
435
+ let count = 0;
436
+ for (let index = 0; index < text.length; index += 1) if (text.charCodeAt(index) === 10) count += 1;
437
+ return count;
357
438
  }
358
439
  /**
359
440
  * Prints the last `lines` of the log, then keeps printing whatever is appended.
@@ -15009,6 +15090,15 @@ var init_log_files = __esmMin((() => {
15009
15090
  }
15010
15091
  if (fs.existsSync(this.currentPath(serverId))) fs.renameSync(this.currentPath(serverId), this.rotatedPath(serverId, 1));
15011
15092
  }
15093
+ /**
15094
+ * The last `tail` parseable lines of one file.
15095
+ *
15096
+ * Reads backwards, growing the window until it has enough lines. It used to read exactly one
15097
+ * fixed chunk, which silently truncated: a JSONL line here is a couple of hundred bytes, so
15098
+ * one 256 KB chunk holds roughly 1500 of them, while the route accepts `tail=5000`. Asking
15099
+ * for 5000 returned 1464 and dropped 3536 lines with no error — the oldest line handed back
15100
+ * was not the oldest asked for.
15101
+ */
15012
15102
  readTailChunk(file, tail) {
15013
15103
  let handle;
15014
15104
  try {
@@ -15018,15 +15108,24 @@ var init_log_files = __esmMin((() => {
15018
15108
  }
15019
15109
  try {
15020
15110
  const size = fs.fstatSync(handle).size;
15021
- const length = Math.min(size, READ_CHUNK_BYTES);
15022
- const buffer = Buffer$1.alloc(length);
15023
- fs.readSync(handle, buffer, 0, length, size - length);
15024
- const raw = buffer.toString("utf8").split("\n").filter((entry) => entry.trim().length > 0);
15025
- const parsed = [];
15026
- for (const entry of raw.slice(size > length ? 1 : 0)) try {
15027
- parsed.push(JSON.parse(entry));
15028
- } catch {}
15029
- return parsed.slice(-tail);
15111
+ if (size === 0) return [];
15112
+ let start = Math.max(0, size - READ_CHUNK_BYTES);
15113
+ let parsed = [];
15114
+ while (true) {
15115
+ const length = size - start;
15116
+ const buffer = Buffer$1.alloc(length);
15117
+ fs.readSync(handle, buffer, 0, length, start);
15118
+ const raw = buffer.toString("utf8").split("\n");
15119
+ parsed = [];
15120
+ for (const entry of start > 0 ? raw.slice(1) : raw) {
15121
+ if (entry.trim().length === 0) continue;
15122
+ try {
15123
+ parsed.push(JSON.parse(entry));
15124
+ } catch {}
15125
+ }
15126
+ if (parsed.length >= tail || start === 0) return parsed.slice(-tail);
15127
+ start = Math.max(0, start - READ_CHUNK_BYTES);
15128
+ }
15030
15129
  } finally {
15031
15130
  fs.closeSync(handle);
15032
15131
  }