Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/runtime/src/__tests__/read-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/

import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { test } from 'node:test';
import {
readPage,
Expand Down Expand Up @@ -175,3 +177,47 @@ test('archive Read preserves fields beside structured text across pages', () =>
assert.deepEqual(JSON.parse(recovered), value);
}
});

test('small line ranges do not need a whole-file line index', async () => {
// 固定堆预算验证实际资源边界,不依赖耗时阈值或具体索引实现。
const { stdout } = await promisify(execFile)(
process.execPath,
[
'--max-old-space-size=64',
'--input-type=module',
'--eval',
`
import assert from 'node:assert/strict';
import { readPage } from ${JSON.stringify(new URL('../read-page.js', import.meta.url).href)};
const content = 'x\\n'.repeat(8_000_000);
const page = readPage(content, { path: 'log.txt', offset: 2_000_000, limit: 20 });
assert.equal(page.totalLines, 8_000_001);
assert.equal(page.offset, 2_000_000);
assert.equal(page.returnedLines, 20);
assert.equal(page.content, 'x\\n'.repeat(19) + 'x');
assert.equal(page.next, null);
console.log('bounded read passed');
`,
],
{ timeout: 30_000 },
);
assert.equal(stdout.trim(), 'bounded read passed');
});

test('line ranges preserve empty lines, trailing newlines and offsets beyond EOF', () => {
for (const content of ['', '\n', '\n\n', 'a\nb', 'a\nb\n', 'a\r\n😀\n\nlast']) {
const lines = content.split('\n');
for (let offset = 0; offset <= lines.length + 1; offset++) {
for (const limit of [undefined, 1, 2, 100]) {
const selected = lines.slice(offset, limit === undefined ? undefined : offset + limit);
assert.deepEqual(readPage(content, { path: 'file.txt', offset, limit }), {
content: selected.join('\n'),
offset,
returnedLines: selected.length,
totalLines: lines.length,
next: null,
});
}
}
}
});
40 changes: 30 additions & 10 deletions packages/runtime/src/read-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,34 @@ export function readPage(
'Invalid Read continuation position. Copy the complete next object from the preceding result.',
);
}
const starts = [0];
for (let at = content.indexOf('\n'); at >= 0; at = content.indexOf('\n', at + 1))
// 总行数仍需扫描全文,但只为本页可能返回的字符保留行索引,避免短行日志
// 为一个小响应分配与整个文件行数成正比的临时数组。
let totalLines = 1;
let offset = resolved.position === undefined ? (input.offset ?? 0) : 0;
let start = resolved.position ?? (offset === 0 ? 0 : content.length);
let lineStart = 0;
let end = content.length;
for (let at = content.indexOf('\n'); at >= 0; at = content.indexOf('\n', at + 1)) {
const line = totalLines++;
if (resolved.position !== undefined && at < resolved.position) {
offset = line;
lineStart = at + 1;
} else if (resolved.position === undefined && line === offset) {
start = at + 1;
lineStart = start;
}
if (input.limit !== undefined && line === offset + input.limit && at >= start) end = at;
}
const lastLine = Math.min(totalLines, offset + (input.limit ?? totalLines));
const starts = [lineStart];
const indexEnd = Math.min(end, start + maxChars);
for (
let at = content.indexOf('\n', start);
at >= 0 && at < indexEnd;
at = content.indexOf('\n', at + 1)
) {
starts.push(at + 1);
const totalLines = starts.length;
}
const lineAt = (position: number): number => {
let low = 0;
let high = starts.length - 1;
Expand All @@ -131,17 +155,13 @@ export function readPage(
if (starts[middle]! <= position) low = middle;
else high = middle - 1;
}
return low;
return offset + low;
};
const offset = resolved.position === undefined ? (input.offset ?? 0) : lineAt(resolved.position);
const start = resolved.position ?? starts[Math.min(offset, totalLines)] ?? content.length;
const lastLine = Math.min(totalLines, offset + (input.limit ?? totalLines));
const end = lastLine < totalLines ? starts[lastLine]! - 1 : content.length;
let digest = resolved.digest;
const makePage = (stop: number): ReadPage => {
const complete = stop >= end;
const nextLine = Math.max(offset, lineAt(stop));
const boundary = stop === (starts[nextLine + 1] ?? content.length + 1) - 1;
const boundary = content[stop] === '\n';
const partialLine = !complete && !boundary;
const returnedLines =
offset >= totalLines
Expand All @@ -163,7 +183,7 @@ export function readPage(
offset,
returnedLines,
totalLines,
...(partialLine || (offset < totalLines && start > starts[offset]!)
...(partialLine || (offset < totalLines && start > lineStart)
? { partialLine: true as const }
: {}),
next,
Expand Down