@vzn/vx 0.0.495 → 0.0.498
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/README.md +8 -10
- package/package.json +5 -5
- package/src/exec/sandbox-runtime.ts +10 -1
- package/src/graph/scheduler.ts +1 -1
- package/src/orchestrator/framed-output.ts +0 -55
- package/src/orchestrator/logger.ts +1 -63
- package/src/orchestrator/run.ts +1 -4
- package/src/orchestrator/failure-recap.ts +0 -121
package/README.md
CHANGED
|
@@ -18,16 +18,14 @@
|
|
|
18
18
|
|
|
19
19
|
<!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
|
|
20
20
|
|
|
21
|
-
| 1,090 packages, 3,270 tasks | vx | Turborepo | Nx
|
|
22
|
-
| -------------------------------- | ---------- | -------------------------- |
|
|
23
|
-
| Cold build: time the runner adds | **
|
|
24
|
-
| Cold build: CPU burned | **
|
|
25
|
-
| Fully cached run | **
|
|
26
|
-
| Overhead per package | **
|
|
27
|
-
|
|
28
|
-
vx
|
|
29
|
-
|
|
30
|
-
Nx's column ran every task through npm run (~200 ms of CPU each; Turbo ran bun run), a harness fault since fixed; with bun, on a 4-core Linux box, Nx took 6m 59s cold and 4.50 s fully cached (vx 47% and 92% faster).
|
|
21
|
+
| 1,090 packages, 3,270 tasks | vx | Turborepo | Nx |
|
|
22
|
+
| -------------------------------- | ---------- | -------------------------- | ---------------------- |
|
|
23
|
+
| Cold build: time the runner adds | **2 s** | 1 min 21 s (vx 34× faster) | 11 s (vx 4.7× faster) |
|
|
24
|
+
| Cold build: CPU burned | **17 s** | 21 s (vx 1.2× faster) | 52 s (vx 3× faster) |
|
|
25
|
+
| Fully cached run | **393 ms** | 463 ms (vx 1.1× faster) | 6.45 s (vx 16× faster) |
|
|
26
|
+
| Overhead per package | **2 ms** | 74 ms (vx 34× faster) | 10 ms (vx 4.7× faster) |
|
|
27
|
+
|
|
28
|
+
vx N× faster: that tool takes N times as long as vx (theirs ÷ vx); N× slower: vx takes N times as long (vx ÷ theirs).
|
|
31
29
|
|
|
32
30
|
Time added is the wall time over the tasks' own ideal schedule (3 min 38 s).
|
|
33
31
|
Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.498",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.498",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.498",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.498",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.498"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
|
@@ -991,7 +991,16 @@ export function resolveSandboxConfig(
|
|
|
991
991
|
(a.write ?? []).map((p) => {
|
|
992
992
|
const real = resolve(p)
|
|
993
993
|
assertWriteStaysHome(p, real, projectDir)
|
|
994
|
-
|
|
994
|
+
// A directory a write glob matches is writable whole, as Linux's
|
|
995
|
+
// bind of the hit makes it: `<glob>/**` collapses below into the
|
|
996
|
+
// glob and its subtree. SRT compiles a glob as an exact regex, so
|
|
997
|
+
// `/tmp/pnpm-store-operation-locks-*/` covered the directory and
|
|
998
|
+
// not the lock file pnpm 12 opens inside it.
|
|
999
|
+
return process.platform !== 'linux' &&
|
|
1000
|
+
!isMountableLiteral(real) &&
|
|
1001
|
+
!/\/\*\*(?:\/\*)?$/.test(real)
|
|
1002
|
+
? `${real}/**`
|
|
1003
|
+
: real
|
|
995
1004
|
}),
|
|
996
1005
|
walls,
|
|
997
1006
|
pending,
|
package/src/graph/scheduler.ts
CHANGED
|
@@ -195,7 +195,7 @@ export interface ScheduleOptions {
|
|
|
195
195
|
/**
|
|
196
196
|
* Where the line naming a rejected `execute` goes, before its failed
|
|
197
197
|
* outcome lands. Absent = stderr. The run gives it the task's own
|
|
198
|
-
* stderr, so its frame
|
|
198
|
+
* stderr, so its frame carries the reason.
|
|
199
199
|
*/
|
|
200
200
|
onError?: (node: TaskNode, line: string) => void
|
|
201
201
|
/**
|
|
@@ -30,7 +30,6 @@ import { maskedCommand } from '../util/index.js'
|
|
|
30
30
|
import { paint, type ColorSupport } from './colors.js'
|
|
31
31
|
import { formatDuration } from './summary.js'
|
|
32
32
|
import { outcomeLabel, skippedReason } from './events.js'
|
|
33
|
-
import type { RecapTail } from './failure-recap.js'
|
|
34
33
|
|
|
35
34
|
const NO_COLOR: ColorSupport = { enabled: false }
|
|
36
35
|
|
|
@@ -498,60 +497,6 @@ export function formatPersistentList(
|
|
|
498
497
|
)
|
|
499
498
|
}
|
|
500
499
|
|
|
501
|
-
/** One failed task's section of the end-of-run recap. */
|
|
502
|
-
export interface RecapEntry {
|
|
503
|
-
node: TaskNode
|
|
504
|
-
outcome: TaskOutcome
|
|
505
|
-
tail: RecapTail
|
|
506
|
-
/** Characters a bounded capture (a persistent task's) dropped before the tail saw them. */
|
|
507
|
-
droppedChars: number
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* The run's last block: each failed task's last lines, under the failure's
|
|
512
|
-
* label, then the ids of the failures past the tail limit. The lines are
|
|
513
|
-
* raw, like a frame's, and `fence` wraps them where a task's text could be
|
|
514
|
-
* read as something other than text (GitHub Actions' workflow commands).
|
|
515
|
-
*/
|
|
516
|
-
export function formatFailureRecap(
|
|
517
|
-
entries: readonly RecapEntry[],
|
|
518
|
-
more: readonly string[],
|
|
519
|
-
colors: ColorSupport = NO_COLOR,
|
|
520
|
-
fence: (lines: string[]) => string[] = (lines) => lines,
|
|
521
|
-
): string[] {
|
|
522
|
-
const total = entries.length + more.length
|
|
523
|
-
const dim = (s: string) => paint('', s, colors, { dim: true })
|
|
524
|
-
const lines = [
|
|
525
|
-
'',
|
|
526
|
-
` Failed: ${total} task${total === 1 ? '' : 's'} — the last lines ${total === 1 ? 'it' : 'each one'} printed`,
|
|
527
|
-
]
|
|
528
|
-
for (const e of entries) {
|
|
529
|
-
lines.push(
|
|
530
|
-
'',
|
|
531
|
-
` ${paint(ERROR, '◼︎', colors)} ${paintTaskId(e.node, colors, { bold: true })} ${dim('—')} ${paint(ERROR, outcomeLabel(e.outcome), colors, { bold: true })}`,
|
|
532
|
-
)
|
|
533
|
-
const cut: string[] = []
|
|
534
|
-
const { earlierLines, cutBytes } = e.tail
|
|
535
|
-
if (earlierLines > 0)
|
|
536
|
-
cut.push(`${count(earlierLines)} earlier line${earlierLines === 1 ? '' : 's'}`)
|
|
537
|
-
if (cutBytes > 0) cut.push(`${count(cutBytes)} bytes cut from the start of the line below`)
|
|
538
|
-
if (e.droppedChars > 0) {
|
|
539
|
-
cut.push(`the capture dropped ${count(e.droppedChars)} characters of the task's output`)
|
|
540
|
-
}
|
|
541
|
-
if (cut.length > 0) lines.push(dim(` … ${cut.join(', and ')}`))
|
|
542
|
-
if (e.tail.text.length === 0) lines.push(dim(' (no output)'))
|
|
543
|
-
else lines.push(...fence(e.tail.text.split('\n')))
|
|
544
|
-
}
|
|
545
|
-
if (more.length > 0) {
|
|
546
|
-
lines.push('', ` … and ${more.length} more failed: ${more.join(', ')}`)
|
|
547
|
-
}
|
|
548
|
-
return lines
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function count(n: number): string {
|
|
552
|
-
return n.toLocaleString('en-US')
|
|
553
|
-
}
|
|
554
|
-
|
|
555
500
|
function formatBlockHeader(o: TaskOutcome, colors: ColorSupport): string {
|
|
556
501
|
const shortHash = o.hash ? o.hash.slice(0, 8) : ''
|
|
557
502
|
const dim = (s: string) => paint('', s, colors, { dim: true })
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { TaskNode, TaskOutcome } from '../graph/index.js'
|
|
2
2
|
import { detectColors, type ColorSupport } from './colors.js'
|
|
3
3
|
import {
|
|
4
|
-
formatFailureRecap,
|
|
5
4
|
formatFrameClose,
|
|
6
5
|
formatFrameOpen,
|
|
7
6
|
formatKeptLines,
|
|
@@ -10,15 +9,7 @@ import {
|
|
|
10
9
|
formatTaskExecutedLine,
|
|
11
10
|
formatTaskHitLine,
|
|
12
11
|
formatTaskSkippedLine,
|
|
13
|
-
type RecapEntry,
|
|
14
12
|
} from './framed-output.js'
|
|
15
|
-
import {
|
|
16
|
-
appendRecapRing,
|
|
17
|
-
createRecapRing,
|
|
18
|
-
RECAP_TASKS,
|
|
19
|
-
recapTail,
|
|
20
|
-
type RecapRing,
|
|
21
|
-
} from './failure-recap.js'
|
|
22
13
|
import {
|
|
23
14
|
createOutputWriter,
|
|
24
15
|
formatFailureLine,
|
|
@@ -63,13 +54,8 @@ export interface Logger {
|
|
|
63
54
|
runEnd?(): void
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
/** The logger `run()` builds when none is passed
|
|
57
|
+
/** The logger `run()` builds when none is passed. */
|
|
67
58
|
export interface DefaultLogger extends Logger {
|
|
68
|
-
/**
|
|
69
|
-
* The run's last block: each failed task's last lines (item 706). Empty
|
|
70
|
-
* when nothing failed, and in the modes that promise no task output.
|
|
71
|
-
*/
|
|
72
|
-
failureRecap(): string[]
|
|
73
59
|
/**
|
|
74
60
|
* Hand over any output still coalesced, and write straight through from
|
|
75
61
|
* now on. `runEnd` does it; the run's own `finally` does it again for a
|
|
@@ -352,13 +338,6 @@ export function defaultLogger(
|
|
|
352
338
|
}
|
|
353
339
|
const printsKept = (tail: { outcome?: TaskOutcome }): boolean =>
|
|
354
340
|
flushedPersistent && tail.outcome !== undefined && view.mode !== 'errors-only'
|
|
355
|
-
// The recap's material, decided at each failure so nothing is held for a
|
|
356
|
-
// task that passed. A buffered task's tail is read off the buffers the
|
|
357
|
-
// frame drains; a live-streamed one keeps no buffer, so a bounded ring
|
|
358
|
-
// follows it from its frame-open until its outcome.
|
|
359
|
-
const recapEntries: RecapEntry[] = []
|
|
360
|
-
const recapMore: string[] = []
|
|
361
|
-
const liveRings = new Map<string, RecapRing>()
|
|
362
341
|
|
|
363
342
|
const refresh = (force: boolean): void => {
|
|
364
343
|
if (statusDead) return
|
|
@@ -457,20 +436,6 @@ export function defaultLogger(
|
|
|
457
436
|
flushKept()
|
|
458
437
|
writer.settle()
|
|
459
438
|
},
|
|
460
|
-
failureRecap() {
|
|
461
|
-
if (recapEntries.length === 0) return []
|
|
462
|
-
// Fenced like a frame: a tail is the task's own text, and a line of it
|
|
463
|
-
// must not become a workflow command. Never inside a group, so it reads
|
|
464
|
-
// with every group collapsed.
|
|
465
|
-
const fence =
|
|
466
|
-
view.gha === true
|
|
467
|
-
? (lines: string[]) =>
|
|
468
|
-
ghaFence(`${lines.join('\n')}\n`, ghaToken)
|
|
469
|
-
.slice(0, -1)
|
|
470
|
-
.split('\n')
|
|
471
|
-
: undefined
|
|
472
|
-
return formatFailureRecap(recapEntries, recapMore, colors, fence)
|
|
473
|
-
},
|
|
474
439
|
status(line) {
|
|
475
440
|
writer.write(`${line}\n`)
|
|
476
441
|
},
|
|
@@ -508,7 +473,6 @@ export function defaultLogger(
|
|
|
508
473
|
// Open the live frame: full task info even when the command
|
|
509
474
|
// streams nothing (or the hit replays nothing).
|
|
510
475
|
emitLine(formatFrameOpen(node, colors))
|
|
511
|
-
liveRings.set(node.id, createRecapRing())
|
|
512
476
|
return
|
|
513
477
|
}
|
|
514
478
|
// Bound a persistent task's output from its FIRST chunk. Waiting for
|
|
@@ -578,8 +542,6 @@ export function defaultLogger(
|
|
|
578
542
|
streamedSinceBlock = true
|
|
579
543
|
if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
|
|
580
544
|
writer.write(chunk)
|
|
581
|
-
const ring = liveRings.get(node.id)
|
|
582
|
-
if (ring !== undefined) appendRecapRing(ring, chunk)
|
|
583
545
|
return
|
|
584
546
|
}
|
|
585
547
|
const tail = persistentTails.get(node.id)
|
|
@@ -596,8 +558,6 @@ export function defaultLogger(
|
|
|
596
558
|
streamedSinceBlock = true
|
|
597
559
|
if (chunk.length > 0) streamMidLine = !chunk.endsWith('\n')
|
|
598
560
|
writer.write(chunk)
|
|
599
|
-
const ring = liveRings.get(node.id)
|
|
600
|
-
if (ring !== undefined) appendRecapRing(ring, chunk)
|
|
601
561
|
return
|
|
602
562
|
}
|
|
603
563
|
const tail = persistentTails.get(node.id)
|
|
@@ -623,28 +583,6 @@ export function defaultLogger(
|
|
|
623
583
|
// The pre-ready window closed either way: a failed task is over, and
|
|
624
584
|
// a ready one starts a fresh post-ready tail below.
|
|
625
585
|
persistentTails.delete(node.id)
|
|
626
|
-
const liveRing = liveRings.get(node.id)
|
|
627
|
-
liveRings.delete(node.id)
|
|
628
|
-
if (outcome.status === 'failed' && !discardsOutput) {
|
|
629
|
-
if (recapEntries.length === RECAP_TASKS) recapMore.push(node.id)
|
|
630
|
-
else {
|
|
631
|
-
let ring = liveRing
|
|
632
|
-
if (ring === undefined) {
|
|
633
|
-
ring = createRecapRing()
|
|
634
|
-
appendRecapRing(ring, stdout)
|
|
635
|
-
if (stdout.length > 0 && stderr.length > 0 && !stdout.endsWith('\n')) {
|
|
636
|
-
appendRecapRing(ring, '\n')
|
|
637
|
-
}
|
|
638
|
-
appendRecapRing(ring, stderr)
|
|
639
|
-
}
|
|
640
|
-
recapEntries.push({
|
|
641
|
-
node,
|
|
642
|
-
outcome,
|
|
643
|
-
tail: recapTail(ring),
|
|
644
|
-
droppedChars: (preReady?.out.dropped ?? 0) + (preReady?.err.dropped ?? 0),
|
|
645
|
-
})
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
586
|
// An aborted task (child killed by a shutdown signal) reverts
|
|
649
587
|
// to pending: free its worker slot, but never count or render it
|
|
650
588
|
// — the run is tearing down and it has no honest outcome.
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -319,7 +319,7 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
319
319
|
}
|
|
320
320
|
let held = false
|
|
321
321
|
try {
|
|
322
|
-
const summary = await runOnBus(options, bus, colors
|
|
322
|
+
const summary = await runOnBus(options, bus, colors)
|
|
323
323
|
if (summary.persistent === undefined) return summary
|
|
324
324
|
// Servers handed back still running still write: `vx watch dev` printed
|
|
325
325
|
// none of its server's log while it idled, the renderer gone with this
|
|
@@ -342,7 +342,6 @@ async function runOnBus(
|
|
|
342
342
|
options: RunOptions,
|
|
343
343
|
bus: EventBus,
|
|
344
344
|
colors: ColorSupport,
|
|
345
|
-
failureRecap: () => string[],
|
|
346
345
|
): Promise<RunSummary> {
|
|
347
346
|
const log = busLogger(bus)
|
|
348
347
|
|
|
@@ -1094,8 +1093,6 @@ async function runOnBus(
|
|
|
1094
1093
|
` Deferred: ${stillDeferred.length} task(s) left outputs remote (--download=none): ${stillDeferred.join(', ')}`,
|
|
1095
1094
|
)
|
|
1096
1095
|
}
|
|
1097
|
-
// Last, where a long log's reader lands: each failure's own last lines.
|
|
1098
|
-
for (const line of failureRecap()) log.status(line)
|
|
1099
1096
|
|
|
1100
1097
|
// Optional artifacts. Errors are surfaced to the user but don't
|
|
1101
1098
|
// change the run's exit code — the run already happened.
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// The end of each failed task's output, kept for the run's last lines
|
|
2
|
-
// (item 706). A failure's frame prints when the task finishes, which in a
|
|
3
|
-
// long CI log is thousands of lines above the end, and GitHub's API returns
|
|
4
|
-
// only a job log's last 5,000 lines: the summary is the one place a reader
|
|
5
|
-
// reliably looks, so the failure's own words are repeated there.
|
|
6
|
-
//
|
|
7
|
-
// The capture is a ring over the output's END: it keeps at most
|
|
8
|
-
// RECAP_TASK_BYTES characters (and a newline) however much a task prints,
|
|
9
|
-
// and counts what it evicts, so the recap can say how much it is not
|
|
10
|
-
// showing.
|
|
11
|
-
|
|
12
|
-
/** Lines of each failed task's output the recap repeats. */
|
|
13
|
-
const RECAP_LINES = 30
|
|
14
|
-
/** The byte cap on one task's repeated lines. */
|
|
15
|
-
const RECAP_TASK_BYTES = 8 * 1024
|
|
16
|
-
/**
|
|
17
|
-
* Tasks that get a tail; the rest are named by id. Five tails at the
|
|
18
|
-
* per-task cap is 40 KiB, so the recap stays under 64 KiB with no cap
|
|
19
|
-
* of its own.
|
|
20
|
-
*/
|
|
21
|
-
export const RECAP_TASKS = 5
|
|
22
|
-
|
|
23
|
-
export interface RecapRing {
|
|
24
|
-
chunks: string[]
|
|
25
|
-
chars: number
|
|
26
|
-
/** Newlines evicted from the head. */
|
|
27
|
-
cutLines: number
|
|
28
|
-
/** Bytes evicted from the line the kept text starts with; 0 when it starts a line. */
|
|
29
|
-
cutPartialBytes: number
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface RecapTail {
|
|
33
|
-
/** The last lines, at most RECAP_LINES of them and RECAP_TASK_BYTES bytes. */
|
|
34
|
-
text: string
|
|
35
|
-
/** Whole lines above `text`. */
|
|
36
|
-
earlierLines: number
|
|
37
|
-
/** Bytes cut from the start of `text`'s first line. */
|
|
38
|
-
cutBytes: number
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function createRecapRing(): RecapRing {
|
|
42
|
-
return { chunks: [], chars: 0, cutLines: 0, cutPartialBytes: 0 }
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// The ring counts characters, the cap bytes: a UTF-16 unit is at least one
|
|
46
|
-
// UTF-8 byte, so the last RECAP_TASK_BYTES characters always hold the last
|
|
47
|
-
// RECAP_TASK_BYTES bytes. One more for the final newline the tail drops.
|
|
48
|
-
const RING_CHARS = RECAP_TASK_BYTES + 1
|
|
49
|
-
|
|
50
|
-
export function appendRecapRing(r: RecapRing, chunk: string): void {
|
|
51
|
-
if (chunk.length === 0) return
|
|
52
|
-
r.chunks.push(chunk)
|
|
53
|
-
r.chars += chunk.length
|
|
54
|
-
// A whole chunk goes only when what follows it still fills the ring:
|
|
55
|
-
// evicting past the cap would leave the recap fewer lines than it shows.
|
|
56
|
-
while (r.chunks.length > 1 && r.chars - r.chunks[0]!.length >= RING_CHARS) {
|
|
57
|
-
const gone = r.chunks.shift()!
|
|
58
|
-
r.chars -= gone.length
|
|
59
|
-
evict(r, gone, gone.length)
|
|
60
|
-
}
|
|
61
|
-
if (r.chars > RING_CHARS) {
|
|
62
|
-
const head = r.chunks[0]!
|
|
63
|
-
const at = r.chars - RING_CHARS
|
|
64
|
-
evict(r, head, at)
|
|
65
|
-
r.chunks[0] = head.slice(at)
|
|
66
|
-
r.chars = RING_CHARS
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function evict(r: RecapRing, s: string, end: number): void {
|
|
71
|
-
const last = s.lastIndexOf('\n', end - 1)
|
|
72
|
-
const partial = Buffer.byteLength(s.slice(last + 1, end))
|
|
73
|
-
if (last < 0) {
|
|
74
|
-
r.cutPartialBytes += partial
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
r.cutLines += countNewlines(s, last + 1)
|
|
78
|
-
r.cutPartialBytes = partial
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function countNewlines(s: string, end: number): number {
|
|
82
|
-
let n = 0
|
|
83
|
-
for (let i = s.indexOf('\n'); i !== -1 && i < end; i = s.indexOf('\n', i + 1)) n++
|
|
84
|
-
return n
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* The recap's view of what the ring kept: its last RECAP_LINES lines, then
|
|
89
|
-
* its last RECAP_TASK_BYTES bytes (cut on a character boundary). The text
|
|
90
|
-
* is a fresh copy, so it holds nothing of the chunks it came from.
|
|
91
|
-
*/
|
|
92
|
-
export function recapTail(r: RecapRing): RecapTail {
|
|
93
|
-
const t = r.chunks.join('')
|
|
94
|
-
const end = t.endsWith('\n') ? t.length - 1 : t.length
|
|
95
|
-
let start = 0
|
|
96
|
-
for (let pos = end, taken = 1; ; pos = start - 1, taken++) {
|
|
97
|
-
const nl = pos === 0 ? -1 : t.lastIndexOf('\n', pos - 1)
|
|
98
|
-
if (nl < 0) {
|
|
99
|
-
start = 0
|
|
100
|
-
break
|
|
101
|
-
}
|
|
102
|
-
start = nl + 1
|
|
103
|
-
if (taken === RECAP_LINES) break
|
|
104
|
-
}
|
|
105
|
-
let earlierLines = r.cutLines + countNewlines(t, start)
|
|
106
|
-
let cutBytes = start === 0 ? r.cutPartialBytes : 0
|
|
107
|
-
const bytes = Buffer.from(t.slice(start, end), 'utf8')
|
|
108
|
-
if (bytes.length <= RECAP_TASK_BYTES)
|
|
109
|
-
return { text: bytes.toString('utf8'), earlierLines, cutBytes }
|
|
110
|
-
let at = bytes.length - RECAP_TASK_BYTES
|
|
111
|
-
while ((bytes[at]! & 0xc0) === 0x80) at++
|
|
112
|
-
const lastNl = bytes.lastIndexOf(0x0a, at - 1)
|
|
113
|
-
if (lastNl < 0) cutBytes += at
|
|
114
|
-
else {
|
|
115
|
-
for (let i = bytes.indexOf(0x0a); i !== -1 && i <= lastNl; i = bytes.indexOf(0x0a, i + 1)) {
|
|
116
|
-
earlierLines++
|
|
117
|
-
}
|
|
118
|
-
cutBytes = at - lastNl - 1
|
|
119
|
-
}
|
|
120
|
-
return { text: bytes.subarray(at).toString('utf8'), earlierLines, cutBytes }
|
|
121
|
-
}
|