buninu-browser 0.2.3 → 0.3.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/README.md +86 -1
- package/lib/cdp-server.js +47 -14
- package/lib/clipboard.js +22 -0
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +40 -11
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +77 -16
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/raw-frames.js +47 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +34 -40
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +133 -98
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +52 -5
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +287 -99
- package/lib/renderer/page-renderer.js +663 -239
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/clipboard.test.js +35 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/raw-capture.test.js +152 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- package/ai-debug.log +0 -5537
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Advance a PCM decoder to an exact sample cursor, never playing discarded PCM.
|
|
2
|
-
* Reopened snapshots start at sample
|
|
2
|
+
* Reopened snapshots start at their epoch's first sample, including its pending chunk.
|
|
3
3
|
* The caller holds output requests until this transaction has completed.
|
|
4
4
|
*/
|
|
5
5
|
export async function advanceAudio(getSound, seconds, { reopen, needsMore, active = () => true }) {
|
|
@@ -10,6 +10,9 @@ export async function advanceAudio(getSound, seconds, { reopen, needsMore, activ
|
|
|
10
10
|
if (sound.position === target) return true;
|
|
11
11
|
if (sound.position > target) {
|
|
12
12
|
if (!await reopen()) return false;
|
|
13
|
+
// A seek before the first retained epoch cannot be satisfied by
|
|
14
|
+
// reopening that same later epoch forever.
|
|
15
|
+
if (getSound().position > target) return false;
|
|
13
16
|
continue;
|
|
14
17
|
}
|
|
15
18
|
// This is also the first chunk of EVERY replacement decoder. Skipping it
|
|
@@ -29,6 +32,12 @@ export async function advanceAudio(getSound, seconds, { reopen, needsMore, activ
|
|
|
29
32
|
continue;
|
|
30
33
|
}
|
|
31
34
|
if (!needsMore(sound)) return false;
|
|
35
|
+
if (Number.isFinite(sound.endTime) && seconds >= sound.endTime) {
|
|
36
|
+
if (!await reopen()) return false;
|
|
37
|
+
const next = getSound();
|
|
38
|
+
if (next.snapshotId === sound.snapshotId && next.openedGeneration === sound.openedGeneration) return false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
32
41
|
const pipe = sound.pipe;
|
|
33
42
|
if (pipe.error) throw pipe.error;
|
|
34
43
|
// Data may already have arrived while this snapshot was being decoded.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createBytePipe } from './byte-pipe.js';
|
|
2
|
+
|
|
3
|
+
// MSE may replace an MP4 initialization segment while old frames are still
|
|
4
|
+
// buffered. A file demuxer cannot consume that sequence as one MP4: keep each
|
|
5
|
+
// initialization and its fragments together, sharing the original byte views.
|
|
6
|
+
export function createSourceSnapshots(inspect) {
|
|
7
|
+
const epochs = [];
|
|
8
|
+
const pending = [];
|
|
9
|
+
let pendingSize = 0, headers = [], candidate = null, current = null;
|
|
10
|
+
let skipMedia = false, serial = 0, fragment = null, openEnded = false;
|
|
11
|
+
const join = parts => parts.length === 1 ? parts[0] : Bun.concatArrayBuffers(parts);
|
|
12
|
+
const bytesOf = parts => {
|
|
13
|
+
const bytes = join(parts);
|
|
14
|
+
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
15
|
+
};
|
|
16
|
+
const same = (a, b) => a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
17
|
+
const take = count => {
|
|
18
|
+
const parts = [];
|
|
19
|
+
pendingSize -= count;
|
|
20
|
+
while (count > 0) {
|
|
21
|
+
const first = pending.shift(), length = Math.min(count, first.length);
|
|
22
|
+
parts.push(first.subarray(0, length));
|
|
23
|
+
if (length < first.length) pending.unshift(first.subarray(length));
|
|
24
|
+
count -= length;
|
|
25
|
+
}
|
|
26
|
+
return bytesOf(parts);
|
|
27
|
+
};
|
|
28
|
+
const peekHeader = () => {
|
|
29
|
+
if (pending[0].length >= Math.min(16, pendingSize)) return pending[0];
|
|
30
|
+
const header = new Uint8Array(Math.min(16, pendingSize));
|
|
31
|
+
let offset = 0;
|
|
32
|
+
for (const part of pending) {
|
|
33
|
+
const length = Math.min(part.length, header.length - offset);
|
|
34
|
+
header.set(part.subarray(0, length), offset); offset += length;
|
|
35
|
+
if (offset === header.length) break;
|
|
36
|
+
}
|
|
37
|
+
return header;
|
|
38
|
+
};
|
|
39
|
+
const activate = start => {
|
|
40
|
+
if (!candidate) return current;
|
|
41
|
+
candidate.startTime = Number.isFinite(start) ? start : 0;
|
|
42
|
+
epochs.push(candidate);
|
|
43
|
+
current = candidate;
|
|
44
|
+
candidate = null;
|
|
45
|
+
return current;
|
|
46
|
+
};
|
|
47
|
+
const accept = (type, bytes) => {
|
|
48
|
+
if (type === 'ftyp') { headers = [bytes]; return true; }
|
|
49
|
+
if (type === 'moov') {
|
|
50
|
+
if (current && same(current.header, bytes)) { headers = []; candidate = null; return false; }
|
|
51
|
+
if (candidate && same(candidate.header, bytes)) { headers = []; return false; }
|
|
52
|
+
const pipe = createBytePipe(), state = {};
|
|
53
|
+
for (const header of headers) pipe.append(header);
|
|
54
|
+
pipe.append(bytes);
|
|
55
|
+
inspect(bytes, state);
|
|
56
|
+
candidate = { id: ++serial, pipe, state, header: bytes.slice(), startTime: 0, covered: [], fragments: [], cached: null };
|
|
57
|
+
headers = [];
|
|
58
|
+
fragment = null;
|
|
59
|
+
skipMedia = false;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (type === 'moof') {
|
|
63
|
+
const epoch = candidate ?? current;
|
|
64
|
+
if (!epoch) { headers.push(bytes); return true; }
|
|
65
|
+
const end = inspect(bytes, epoch.state), start = epoch.state.appendStart;
|
|
66
|
+
activate(start);
|
|
67
|
+
skipMedia = Number.isFinite(start) && Number.isFinite(end)
|
|
68
|
+
&& current.covered.some(([from, to]) => from <= start + 0.002 && to + 0.002 >= end);
|
|
69
|
+
if (!skipMedia) {
|
|
70
|
+
if (Number.isFinite(start)) current.startTime = Math.min(current.startTime, start);
|
|
71
|
+
fragment = { start: Number.isFinite(start) ? start : current.startTime, pipe: createBytePipe() };
|
|
72
|
+
fragment.pipe.append(bytes);
|
|
73
|
+
current.fragments.push(fragment);
|
|
74
|
+
current.cached = null;
|
|
75
|
+
if (Number.isFinite(start) && Number.isFinite(end)) current.covered.push([start, end]);
|
|
76
|
+
}
|
|
77
|
+
return !skipMedia;
|
|
78
|
+
}
|
|
79
|
+
if (type === 'mdat') {
|
|
80
|
+
activate(0); // A non-fragmented MP4 has no tfdt/moof timeline.
|
|
81
|
+
if (!current) { headers.push(bytes); return true; }
|
|
82
|
+
if (!skipMedia) { (fragment?.pipe ?? current.pipe).append(bytes); current.cached = null; }
|
|
83
|
+
return !skipMedia;
|
|
84
|
+
}
|
|
85
|
+
if (candidate) candidate.pipe.append(bytes);
|
|
86
|
+
else if (current && !skipMedia) { (fragment?.pipe ?? current.pipe).append(bytes); current.cached = null; }
|
|
87
|
+
else headers.push(bytes);
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
append(bytes) {
|
|
92
|
+
if (openEnded && current) { (fragment?.pipe ?? current.pipe).append(bytes); current.cached = null; return true; }
|
|
93
|
+
if (bytes.length) { pending.push(bytes); pendingSize += bytes.length; }
|
|
94
|
+
let accepted = false;
|
|
95
|
+
while (pendingSize >= 8) {
|
|
96
|
+
const header = peekHeader(), view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
97
|
+
let size = view.getUint32(0), headerSize = 8;
|
|
98
|
+
const type = String.fromCharCode(...header.subarray(4, 8));
|
|
99
|
+
// A zero-sized final mdat extends to EOF, including later appends.
|
|
100
|
+
if (size === 0 && type === 'mdat') { size = pendingSize; openEnded = true; }
|
|
101
|
+
if (size === 1) {
|
|
102
|
+
if (pendingSize < 16) break;
|
|
103
|
+
const large = view.getBigUint64(8);
|
|
104
|
+
if (large > BigInt(Number.MAX_SAFE_INTEGER)) break;
|
|
105
|
+
size = Number(large); headerSize = 16;
|
|
106
|
+
}
|
|
107
|
+
if (size < headerSize || size > pendingSize) break;
|
|
108
|
+
accepted = accept(type, take(size)) || accepted;
|
|
109
|
+
}
|
|
110
|
+
return accepted || pendingSize > 0;
|
|
111
|
+
},
|
|
112
|
+
at(seconds = 0) {
|
|
113
|
+
let selected = null;
|
|
114
|
+
for (const epoch of epochs) {
|
|
115
|
+
if (epoch.startTime <= seconds + 1e-6 && (!selected || epoch.startTime >= selected.startTime)) selected = epoch;
|
|
116
|
+
}
|
|
117
|
+
selected ??= epochs[0] ?? candidate;
|
|
118
|
+
if (!selected) return { id: 0, startTime: 0, endTime: Infinity, bytes: bytesOf([...headers, ...pending]) };
|
|
119
|
+
let endTime = Infinity;
|
|
120
|
+
for (const epoch of epochs) if (epoch.startTime > selected.startTime && epoch.startTime < endTime) endTime = epoch.startTime;
|
|
121
|
+
selected.cached ??= bytesOf([selected.pipe.bytes(), ...selected.fragments.slice()
|
|
122
|
+
.sort((a, b) => a.start - b.start).map(fragment => fragment.pipe.bytes())]);
|
|
123
|
+
return { id: selected.id, startTime: selected.startTime, endTime, bytes: selected.cached };
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// wasmpeg seeks to a keyframe without returning its timestamp. Locate it by
|
|
2
2
|
// comparing tiny decoded probes, then rewind before delivering full frames.
|
|
3
|
-
export function seekVideoKeyframe(decoder, seconds) {
|
|
3
|
+
export function seekVideoKeyframe(decoder, seconds, startTime = 0) {
|
|
4
|
+
const start = Math.max(0, startTime);
|
|
5
|
+
seconds = Math.max(start, seconds);
|
|
4
6
|
const seek = time => decoder.seek(Math.max(1, time * 1000));
|
|
5
7
|
const frame = 1 / (decoder.fps || 30);
|
|
6
|
-
if (!(seconds >= frame)) { seek(
|
|
8
|
+
if (!(seconds >= start + frame)) { seek(start); return start; }
|
|
7
9
|
const probe = time => {
|
|
8
10
|
seek(time);
|
|
9
11
|
const pixels = decoder.nextFrame(32, 18);
|
|
@@ -15,8 +17,19 @@ export function seekVideoKeyframe(decoder, seconds) {
|
|
|
15
17
|
const target = probe(seconds);
|
|
16
18
|
let keyframe = seconds;
|
|
17
19
|
if (target !== null) {
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
+
let distance = 10;
|
|
21
|
+
let earlier = Math.max(start, seconds - distance);
|
|
22
|
+
let previous = probe(earlier);
|
|
23
|
+
// A GOP can span more than ten seconds. Keep widening the search until
|
|
24
|
+
// it crosses the keyframe or reaches the start; otherwise the reported
|
|
25
|
+
// time moves forward while the decoder still returns that older frame.
|
|
26
|
+
while (earlier > start && previous === target) {
|
|
27
|
+
keyframe = earlier;
|
|
28
|
+
distance *= 2;
|
|
29
|
+
earlier = Math.max(start, seconds - distance);
|
|
30
|
+
previous = probe(earlier);
|
|
31
|
+
}
|
|
32
|
+
if (previous !== target) {
|
|
20
33
|
while (keyframe - earlier > frame) {
|
|
21
34
|
const middle = (earlier + keyframe) / 2;
|
|
22
35
|
if (probe(middle) === target) keyframe = middle;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** EBML element header, without reading or scanning the element's payload. */
|
|
2
|
+
export function ebmlHeader(bytes, offset = 0) {
|
|
3
|
+
const vint = (at, id = false) => {
|
|
4
|
+
if (at >= bytes.length || bytes[at] === 0) return null;
|
|
5
|
+
let length = 1, mask = 0x80;
|
|
6
|
+
while (!(bytes[at] & mask)) { length++; mask >>= 1; }
|
|
7
|
+
if (at + length > bytes.length) return null;
|
|
8
|
+
let value = id ? bytes[at] : bytes[at] & (mask - 1);
|
|
9
|
+
let unknown = !id && value === mask - 1;
|
|
10
|
+
for (let i = 1; i < length; i++) {
|
|
11
|
+
value = value * 256 + bytes[at + i];
|
|
12
|
+
unknown &&= bytes[at + i] === 0xff;
|
|
13
|
+
}
|
|
14
|
+
return { length, value, unknown };
|
|
15
|
+
};
|
|
16
|
+
const id = vint(offset, true);
|
|
17
|
+
const size = id && vint(offset + id.length);
|
|
18
|
+
if (!size || (!size.unknown && !Number.isSafeInteger(size.value))) return null;
|
|
19
|
+
return { id: id.value, length: id.length + size.length, size: size.unknown ? Infinity : size.value };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SEGMENT = 0x18538067, CLUSTER = 0x1f43b675;
|
|
23
|
+
// An unknown-size Cluster ends at another Segment child, never at an ID
|
|
24
|
+
// which merely happens to occur inside an encoded audio/video packet.
|
|
25
|
+
const LEVEL_ONE = new Set([0x1a45dfa3, SEGMENT, CLUSTER, 0x114d9b74,
|
|
26
|
+
0x1549a966, 0x1654ae6b, 0x1c53bb6b, 0x1941a469, 0x1043a770, 0x1254c367]);
|
|
27
|
+
|
|
28
|
+
/** Reassembles complete WebM elements across arbitrary SourceBuffer appends.
|
|
29
|
+
* Segment headers are emitted separately; their children (especially each
|
|
30
|
+
* Cluster) remain separate so retries can be discarded without new media.
|
|
31
|
+
* Pending input is kept as views and copied only when an element completes.
|
|
32
|
+
*/
|
|
33
|
+
export function createWebMStream() {
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let available = 0, unknown = null;
|
|
36
|
+
const peek = count => {
|
|
37
|
+
count = Math.min(count, available);
|
|
38
|
+
if (chunks[0]?.length >= count) return chunks[0].subarray(0, count);
|
|
39
|
+
const bytes = new Uint8Array(count);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
for (const chunk of chunks) {
|
|
42
|
+
const n = Math.min(chunk.length, count - offset);
|
|
43
|
+
bytes.set(chunk.subarray(0, n), offset); offset += n;
|
|
44
|
+
if (offset === count) break;
|
|
45
|
+
}
|
|
46
|
+
return bytes;
|
|
47
|
+
};
|
|
48
|
+
const take = count => {
|
|
49
|
+
const bytes = peek(count);
|
|
50
|
+
available -= count;
|
|
51
|
+
for (let left = count; left > 0;) {
|
|
52
|
+
const chunk = chunks[0];
|
|
53
|
+
if (left < chunk.length) { chunks[0] = chunk.subarray(left); break; }
|
|
54
|
+
chunks.shift(); left -= chunk.length;
|
|
55
|
+
}
|
|
56
|
+
return bytes;
|
|
57
|
+
};
|
|
58
|
+
const parse = (final = false) => {
|
|
59
|
+
const result = [];
|
|
60
|
+
while (available) {
|
|
61
|
+
const header = ebmlHeader(peek(16));
|
|
62
|
+
if (!header) break;
|
|
63
|
+
if (unknown && LEVEL_ONE.has(header.id)) {
|
|
64
|
+
result.push({ closedCluster: unknown }); unknown = null; continue;
|
|
65
|
+
}
|
|
66
|
+
if (unknown) {
|
|
67
|
+
const size = header.length + header.size;
|
|
68
|
+
if (!Number.isFinite(size) || size > available) break;
|
|
69
|
+
const bytes = take(size);
|
|
70
|
+
if (header.id === 0xe7) {
|
|
71
|
+
let time = 0;
|
|
72
|
+
for (let i = header.length; i < bytes.length; i++) time = time * 256 + bytes[i];
|
|
73
|
+
unknown.time = time;
|
|
74
|
+
unknown.prefix.push(bytes);
|
|
75
|
+
} else if (header.id === 0xa3 || header.id === 0xa0) {
|
|
76
|
+
// Live streams may use one Cluster indefinitely. A complete Block
|
|
77
|
+
// is independently delimited, so expose it without waiting for the
|
|
78
|
+
// next Cluster. The writer inserts its original prefix only once.
|
|
79
|
+
result.push({ id: CLUSTER, bytes, cluster: { token: unknown, time: unknown.time, prefix: unknown.prefix } });
|
|
80
|
+
unknown.delivered = true;
|
|
81
|
+
} else if (!unknown.delivered) unknown.prefix.push(bytes);
|
|
82
|
+
} else if (header.id === SEGMENT) {
|
|
83
|
+
result.push({ id: SEGMENT, bytes: take(header.length) });
|
|
84
|
+
} else if (header.id === CLUSTER && !Number.isFinite(header.size)) {
|
|
85
|
+
unknown = { prefix: [take(header.length)], time: 0, delivered: false, accepted: false };
|
|
86
|
+
} else {
|
|
87
|
+
const size = header.length + header.size;
|
|
88
|
+
if (!Number.isFinite(size) || size > available) break;
|
|
89
|
+
result.push({ id: header.id, bytes: take(size) });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (final && unknown && !available) { result.push({ closedCluster: unknown }); unknown = null; }
|
|
93
|
+
return result;
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
append(bytes) {
|
|
97
|
+
if (bytes.length) { chunks.push(bytes); available += bytes.length; }
|
|
98
|
+
return parse();
|
|
99
|
+
},
|
|
100
|
+
finish() { return parse(true); },
|
|
101
|
+
get pendingSize() {
|
|
102
|
+
return available + (unknown && !unknown.accepted ? unknown.prefix.reduce((sum, part) => sum + part.length, 0) : 0);
|
|
103
|
+
},
|
|
104
|
+
reset() { chunks.length = 0; available = 0; unknown = null; },
|
|
105
|
+
};
|
|
106
|
+
}
|