buninu-browser 0.5.1 → 0.6.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 +9 -3
- package/lib/audio/pcm.js +11 -0
- package/lib/audio/pulse-client.js +7 -2
- package/lib/cdp-server.js +31 -7
- package/lib/happy-dom/document-base.js +29 -1
- package/lib/happy-dom/mutation-observer.js +157 -0
- package/lib/happy-dom/parser.js +2 -0
- package/lib/headless-shell.js +49 -9
- package/lib/layout/repaint.js +89 -0
- package/lib/media/mp4-keyframes.js +144 -0
- package/lib/media/video-keyframe.js +29 -1
- package/lib/media/webm-snapshots.js +282 -0
- package/lib/network/cookie-jar.js +18 -2
- package/lib/paint/display-list.js +4 -3
- package/lib/paint/frame.js +12 -5
- package/lib/render-tree/index.js +3 -0
- package/lib/renderer/animation-frames.js +76 -0
- package/lib/renderer/bindings.js +2 -0
- package/lib/renderer/compositor-channel.js +95 -10
- package/lib/renderer/compositor.js +54 -18
- package/lib/renderer/document-cookies.js +87 -0
- package/lib/renderer/font-loading.js +60 -16
- package/lib/renderer/host.js +48 -19
- package/lib/renderer/idle-callbacks.js +109 -0
- package/lib/renderer/main.js +5 -1
- package/lib/renderer/media-source.js +4 -0
- package/lib/renderer/page-realm.js +159 -49
- package/lib/renderer/page-renderer.js +567 -197
- package/lib/renderer/resource-elements.js +88 -0
- package/lib/renderer/script-rewrite.js +18 -1
- package/lib/renderer/seccomp.js +6 -4
- package/lib/renderer/timers.js +71 -0
- package/lib/renderer/video-channel.js +94 -0
- package/lib/renderer/video-mask.js +115 -0
- package/lib/renderer/video-presentation.js +304 -39
- package/lib/renderer/video-worker.js +46 -0
- package/lib/renderer/workers.js +7 -17
- package/lib/style/computed-style.js +257 -61
- package/package.json +1 -1
- package/test/animation-frames.test.js +67 -0
- package/test/canvas-style-snapshot.test.js +18 -0
- package/test/cdp-stream.test.js +96 -3
- package/test/compositor-channel-ownership.test.js +194 -0
- package/test/compositor.test.js +27 -1
- package/test/computed-style-binding.test.js +49 -0
- package/test/computed-style-connectivity.test.js +13 -0
- package/test/computed-style.test.js +86 -0
- package/test/cookie-sync.test.js +82 -0
- package/test/document-base-cache.test.js +103 -0
- package/test/document-cookies.test.js +117 -0
- package/test/duplicate-style-keys.test.js +11 -0
- package/test/fixtures/animation-frames.js +56 -0
- package/test/fixtures/canvas-style-snapshot.js +81 -0
- package/test/fixtures/computed-style-connectivity.js +101 -0
- package/test/fixtures/computed-style-lazy.js +135 -0
- package/test/fixtures/duplicate-style-keys.js +97 -0
- package/test/fixtures/fake-renderer.js +12 -0
- package/test/fixtures/font-definition-invalidation.js +97 -0
- package/test/fixtures/font-metrics-invalidation.js +87 -0
- package/test/fixtures/full-style-identity.js +36 -0
- package/test/fixtures/hidden-style-engine.js +260 -0
- package/test/fixtures/hidden-style-reentry.js +232 -0
- package/test/fixtures/hidden-style-renderer.js +203 -0
- package/test/fixtures/layout-repaint.js +69 -0
- package/test/fixtures/media-presentation-continuation.js +141 -0
- package/test/fixtures/media-presentation-refill.js +113 -0
- package/test/fixtures/media-probe.js +148 -0
- package/test/fixtures/media-refill-cancel.js +79 -0
- package/test/fixtures/media-repaint.js +48 -0
- package/test/fixtures/media-resource-reselection.js +94 -0
- package/test/fixtures/media-source-attachment.js +136 -0
- package/test/fixtures/media-source-no-decoder.js +17 -0
- package/test/fixtures/media-track-refill.js +132 -0
- package/test/fixtures/media-webm-seek.js +51 -0
- package/test/fixtures/mutation-observer.js +201 -0
- package/test/fixtures/observer-invalidation.js +105 -0
- package/test/fixtures/prepaint-scheduling.js +73 -0
- package/test/fixtures/resource-discovery.js +32 -0
- package/test/fixtures/spare-renderer-scheduling.js +124 -0
- package/test/fixtures/style-propagation.js +26 -0
- package/test/fixtures/svg-node-reuse.js +83 -0
- package/test/fixtures/video-black-replay.js +140 -0
- package/test/fixtures/video-controls-invalidation.js +71 -0
- package/test/fixtures/viewport-bounds.js +148 -0
- package/test/font-loading.test.js +77 -0
- package/test/font-metrics-invalidation.test.js +60 -0
- package/test/full-style-identity.test.js +191 -0
- package/test/hidden-style-lazy.test.js +31 -0
- package/test/hidden-style-reentry.test.js +20 -0
- package/test/idle-callback.test.js +89 -0
- package/test/idle-scheduler.test.js +148 -0
- package/test/inline-style-cascade.test.js +63 -0
- package/test/layout-repaint-renderer.test.js +9 -0
- package/test/layout-repaint.test.js +222 -0
- package/test/media-presentation-continuation.test.js +14 -0
- package/test/media-presentation-refill.test.js +8 -0
- package/test/media-probe.test.js +16 -0
- package/test/media-repaint.test.js +8 -0
- package/test/media-source-attachment.test.js +34 -0
- package/test/media-track-refill.test.js +26 -0
- package/test/media-webm-seek.test.js +11 -0
- package/test/mp4-keyframes.test.js +36 -0
- package/test/mutation-observer.test.js +91 -0
- package/test/observer-invalidation.test.js +12 -0
- package/test/page-scripts.test.js +35 -0
- package/test/page-timers.test.js +67 -0
- package/test/paint-frame.test.js +18 -1
- package/test/pcm.test.js +63 -0
- package/test/prepaint-scheduling.test.js +13 -0
- package/test/pulse-client.test.js +165 -0
- package/test/raw-capture.test.js +34 -5
- package/test/renderer-host.test.js +51 -0
- package/test/resource-discovery.test.js +12 -0
- package/test/resource-elements.test.js +234 -0
- package/test/root-self-style.test.js +104 -0
- package/test/script-rewrite.test.js +35 -0
- package/test/seccomp.test.js +45 -0
- package/test/spare-renderer-scheduling.test.js +8 -0
- package/test/style-propagation.test.js +225 -0
- package/test/style-sheets.test.js +19 -0
- package/test/svg.test.js +45 -0
- package/test/timers.test.js +58 -0
- package/test/video-black-replay.test.js +34 -0
- package/test/video-capture.test.js +170 -0
- package/test/video-channel.test.js +253 -0
- package/test/video-continuation.test.js +57 -0
- package/test/video-controls-invalidation.test.js +12 -0
- package/test/video-keyframe.test.js +104 -4
- package/test/video-mask.test.js +94 -0
- package/test/video-presentation.test.js +239 -1
- package/test/video-timing-presentation.test.js +115 -0
- package/test/viewport-bounds.test.js +13 -0
- package/test/webm-snapshots.test.js +372 -0
|
@@ -12,7 +12,7 @@ export function encodePacket(message, pixels = null) {
|
|
|
12
12
|
const json = Buffer.from(JSON.stringify(message));
|
|
13
13
|
const size = pixels?.byteLength ?? 0;
|
|
14
14
|
if (json.length > MAX_JSON || size > MAX_PIXELS || json.length + size > MAX_QUEUE) throw Error('Compositor packet too large');
|
|
15
|
-
const packet = Buffer.
|
|
15
|
+
const packet = Buffer.allocUnsafeSlow(8 + json.length + size);
|
|
16
16
|
packet.writeUInt32LE(json.length, 0); packet.writeUInt32LE(size, 4);
|
|
17
17
|
packet.set(json, 8); if (size) packet.set(pixels, 8 + json.length);
|
|
18
18
|
return packet;
|
|
@@ -34,7 +34,7 @@ export class PacketReader {
|
|
|
34
34
|
const binaryLength = this.header.readUInt32LE(4);
|
|
35
35
|
const total = this.jsonLength + binaryLength;
|
|
36
36
|
if (!this.jsonLength || this.jsonLength > MAX_JSON || binaryLength > MAX_PIXELS || total > this.limit) throw Error('Invalid compositor packet size');
|
|
37
|
-
this.packet = Buffer.
|
|
37
|
+
this.packet = Buffer.allocUnsafeSlow(total);
|
|
38
38
|
} else {
|
|
39
39
|
const packet = this.packet; this.packet = null;
|
|
40
40
|
const message = JSON.parse(packet.subarray(0, this.jsonLength).toString());
|
|
@@ -45,21 +45,106 @@ export class PacketReader {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Serialize complete packets, including their two writes, so control messages
|
|
49
|
+
// cannot land between an owned frame's header and pixels. Do not cork these
|
|
50
|
+
// writes: Bun's node:net _writev joins chunks with Buffer.concat.
|
|
51
|
+
export class PacketWriter {
|
|
52
|
+
constructor(socket, limit = MAX_QUEUE) {
|
|
53
|
+
this.socket = socket; this.limit = limit; this.queue = []; this.bytes = 0;
|
|
54
|
+
this.current = null; this.waiting = false; this.pumping = false; this.closed = false;
|
|
55
|
+
socket.on('error', () => this.close());
|
|
56
|
+
socket.once('close', () => {
|
|
57
|
+
this.close();
|
|
58
|
+
// A destroyed socket can still have a pending native write until its
|
|
59
|
+
// callback/close event. Only now is its last borrowed buffer disposable.
|
|
60
|
+
this.release(this.current); this.current = null;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
release(entry) {
|
|
64
|
+
if (!entry || entry.released) return;
|
|
65
|
+
entry.released = true;
|
|
66
|
+
if (entry.queued) this.bytes -= entry.size;
|
|
67
|
+
for (const buffer of entry.buffers) buffer.transfer(0);
|
|
68
|
+
}
|
|
69
|
+
close() {
|
|
70
|
+
if (this.closed) return;
|
|
71
|
+
this.closed = true;
|
|
72
|
+
for (const entry of this.queue) this.release(entry);
|
|
73
|
+
this.queue.length = 0;
|
|
74
|
+
this.socket.destroy();
|
|
75
|
+
}
|
|
76
|
+
pump() {
|
|
77
|
+
if (this.pumping || this.waiting || this.closed) return;
|
|
78
|
+
this.pumping = true;
|
|
79
|
+
try {
|
|
80
|
+
while (!this.waiting && !this.closed) {
|
|
81
|
+
this.current ??= this.queue.shift();
|
|
82
|
+
const entry = this.current;
|
|
83
|
+
if (!entry) break;
|
|
84
|
+
if (entry.index === entry.parts.length) {
|
|
85
|
+
this.release(entry); this.current = null; continue;
|
|
86
|
+
}
|
|
87
|
+
this.waiting = true;
|
|
88
|
+
try {
|
|
89
|
+
this.socket.write(entry.parts[entry.index++], error => {
|
|
90
|
+
if (entry !== this.current || entry.released) return;
|
|
91
|
+
this.waiting = false;
|
|
92
|
+
if (error || this.closed || this.socket.destroyed) {
|
|
93
|
+
this.close(); this.release(entry); this.current = null;
|
|
94
|
+
} else this.pump();
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Some header bytes may already be on the wire. Closing is required
|
|
98
|
+
// even when write throws synchronously, or the next packet is corrupt.
|
|
99
|
+
this.close();
|
|
100
|
+
return error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} finally { this.pumping = false; }
|
|
104
|
+
}
|
|
105
|
+
send(message, pixels, {takeOwnership = false} = {}) {
|
|
106
|
+
const entry = {parts:[], buffers:[], index:0, size:0, queued:false, released:false};
|
|
107
|
+
try {
|
|
108
|
+
if (takeOwnership) {
|
|
109
|
+
if (!(pixels instanceof Uint8Array) || !(pixels.buffer instanceof ArrayBuffer))
|
|
110
|
+
throw TypeError('Owned compositor pixels must have a private ArrayBuffer');
|
|
111
|
+
// The caller gives up this entire buffer, including on a rejected send.
|
|
112
|
+
entry.buffers.push(pixels.buffer);
|
|
113
|
+
}
|
|
114
|
+
if (this.closed || this.socket.destroyed) throw Error('Compositor channel closed');
|
|
115
|
+
const packet = encodePacket(message, takeOwnership ? null : pixels);
|
|
116
|
+
entry.buffers.push(packet.buffer);
|
|
117
|
+
entry.parts.push(packet);
|
|
118
|
+
if (takeOwnership) {
|
|
119
|
+
const size = pixels.byteLength;
|
|
120
|
+
if (size > MAX_PIXELS || packet.length - 8 + size > MAX_QUEUE) throw Error('Compositor packet too large');
|
|
121
|
+
packet.writeUInt32LE(size, 4);
|
|
122
|
+
if (size) entry.parts.push(pixels);
|
|
123
|
+
}
|
|
124
|
+
entry.size = entry.parts.reduce((size, part) => size + part.byteLength, 0);
|
|
125
|
+
if (this.bytes + entry.size > this.limit) {
|
|
126
|
+
this.close(); throw Error('Compositor channel backpressure limit');
|
|
127
|
+
}
|
|
128
|
+
entry.queued = true; this.bytes += entry.size; this.queue.push(entry);
|
|
129
|
+
const error = this.pump();
|
|
130
|
+
if (error) throw error;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (!entry.queued) this.release(entry);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
48
138
|
function attach(socket, receive, closed, limit) {
|
|
49
139
|
socket.setNoDelay(true);
|
|
50
140
|
const reader = new PacketReader(receive, limit);
|
|
141
|
+
const writer = new PacketWriter(socket);
|
|
51
142
|
socket.on('data', chunk => { try { reader.push(chunk); } catch { socket.destroy(); } });
|
|
52
|
-
socket.on('error', () => {});
|
|
53
143
|
socket.once('close', closed);
|
|
54
144
|
return {
|
|
55
145
|
reader,
|
|
56
|
-
send(message, pixels) {
|
|
57
|
-
|
|
58
|
-
const packet = encodePacket(message, pixels);
|
|
59
|
-
if (socket.writableLength + packet.length > MAX_QUEUE) { socket.destroy(); throw Error('Compositor channel backpressure limit'); }
|
|
60
|
-
socket.write(packet);
|
|
61
|
-
},
|
|
62
|
-
close() { socket.destroy(); },
|
|
146
|
+
send(message, pixels, options) { writer.send(message, pixels, options); },
|
|
147
|
+
close() { writer.close(); },
|
|
63
148
|
};
|
|
64
149
|
}
|
|
65
150
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* controller -> compositor { id, method: "screenshot" | "scroll" | "scrollOffset", params }
|
|
12
12
|
* (screenshot params: [{ format?: "png" | "jpeg" | "webp", quality? }])
|
|
13
13
|
* compositor -> controller { id, result } | { id, error }
|
|
14
|
-
* { type: "frame" } (
|
|
14
|
+
* { type: "frame", scrollOffset: { x, y } } (the viewport changed)
|
|
15
15
|
*
|
|
16
16
|
* Messages with the main thread (postMessage):
|
|
17
17
|
* main -> compositor { type: "frame", serial, tile, viewport, content, scroll, scrollEpoch }
|
|
@@ -25,8 +25,8 @@ import { ScrollViewport } from "../layout/scroll-viewport.js";
|
|
|
25
25
|
import { rawFrame, captureViewport, encodeImage, encodeImageBase64 } from "../paint/frame.js";
|
|
26
26
|
import { FrameCaptureCache } from "../paint/frame-cache.js";
|
|
27
27
|
import { FrameLeaseTracker } from "../paint/shared-frame-pool.js";
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
28
|
+
import { startVideoChannel } from "./video-channel.js";
|
|
29
|
+
import { compositeVideo, captureVideoViewport } from "./video-presentation.js";
|
|
30
30
|
|
|
31
31
|
import {connectCompositor} from './compositor-channel.js';
|
|
32
32
|
|
|
@@ -36,9 +36,14 @@ let scrollEpoch = -1;
|
|
|
36
36
|
let frameSerial = 0;
|
|
37
37
|
const captures = new FrameCaptureCache();
|
|
38
38
|
const frameLeases = new FrameLeaseTracker(serial => postMessage({ type: "frame-release", serial }));
|
|
39
|
-
|
|
39
|
+
// Register before awaiting decoder startup: the parent's connect message may
|
|
40
|
+
// arrive while the media worker is loading WASM.
|
|
41
|
+
const endpointReady = new Promise(resolve => {
|
|
42
|
+
self.onmessage = ({data}) => { if (data?.type === 'connect') resolve(data.endpoint); };
|
|
43
|
+
});
|
|
44
|
+
const video = await startVideoChannel(() => {
|
|
40
45
|
frameSerial++;
|
|
41
|
-
|
|
46
|
+
notifyFrame();
|
|
42
47
|
}, report => postMessage({ type: "video-report", ...report }));
|
|
43
48
|
|
|
44
49
|
let channel;
|
|
@@ -46,16 +51,23 @@ function send(message) {
|
|
|
46
51
|
const result = message.result;
|
|
47
52
|
if (result?.pixels instanceof Uint8Array) {
|
|
48
53
|
const {pixels,...metadata} = result;
|
|
49
|
-
|
|
54
|
+
// This raw result is exclusively owned here. The transport borrows it
|
|
55
|
+
// directly, then releases it after the write completes (or the socket
|
|
56
|
+
// closes); do not detach bytes while a native socket may still use them.
|
|
57
|
+
channel.send({...message,result:metadata},pixels,{takeOwnership:true});
|
|
50
58
|
} else channel.send(message);
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
function notifyFrame() {
|
|
61
|
+
// Snapshot the compositor-owned position with its invalidation. A zero-area
|
|
62
|
+
// screencast needs no pixels and can use this without another socket request.
|
|
63
|
+
send({ type: "frame", scrollOffset: { x: viewport.x, y: viewport.y } });
|
|
64
|
+
}
|
|
65
|
+
const endpoint = await endpointReady;
|
|
55
66
|
channel = await connectCompositor(endpoint, request => { void handleRequest(request); }, () => process.exit(1));
|
|
56
67
|
|
|
57
68
|
self.onmessage = ({ data }) => {
|
|
58
69
|
if (data?.type === "video-open") { void video.open(data); return; }
|
|
70
|
+
if (data?.type === "video-extend") { video.extend(data); return; }
|
|
59
71
|
if (data?.type === "video-stop") { video.stop(data.id); frameSerial++; return; }
|
|
60
72
|
if (data?.type === "video-bind") { video.bind(data.id, data.audioId, data.audioStart, data.time, data.endTime); return; }
|
|
61
73
|
if (data?.type !== "frame") return;
|
|
@@ -75,33 +87,58 @@ self.onmessage = ({ data }) => {
|
|
|
75
87
|
postMessage({ type: "scroll", x: viewport.x, y: viewport.y });
|
|
76
88
|
}
|
|
77
89
|
}
|
|
78
|
-
|
|
90
|
+
notifyFrame();
|
|
79
91
|
// Lets the main thread answer the call that produced this frame only once
|
|
80
92
|
// screenshots taken after it will show it.
|
|
81
93
|
postMessage({ type: "frame-ack", serial: data.serial });
|
|
82
94
|
};
|
|
83
95
|
|
|
96
|
+
// Only private composition/crop buffers are disposable. A full-width crop
|
|
97
|
+
// may borrow the immutable raster tile, which remains protected by its lease.
|
|
98
|
+
function releaseCapture(frame, composited, captured, retained = null) {
|
|
99
|
+
if (captured && captured.pixels.buffer !== retained && captured.pixels.buffer !== composited.pixels.buffer)
|
|
100
|
+
captured.pixels.buffer.transfer(0);
|
|
101
|
+
if (composited !== frame && composited.pixels.buffer !== retained) composited.pixels.buffer.transfer(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
84
104
|
const methods = {
|
|
85
105
|
async screenshot({ format = "png", quality = 80, optimizeForSpeed = false, clip } = {}) {
|
|
86
106
|
if (!tile) return "";
|
|
87
107
|
if (format === 'rgba' || format === 'bgra') {
|
|
88
108
|
return frameLeases.capture(frame => {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
const visible = frame.videoLayer?.id === video.config?.id
|
|
110
|
+
? captureVideoViewport(frame, video.frame, viewport.x, viewport.y, viewport.width, viewport.height, clip, format) : null;
|
|
111
|
+
if (visible) {
|
|
112
|
+
// This crop owns its bytes. The normal transport releases them
|
|
113
|
+
// after the socket write completes; the raster tile's lease stays intact.
|
|
114
|
+
try { return rawFrame(visible.pixels, visible.width, visible.height, format,
|
|
115
|
+
{takeOwnership: true, inputFormat: visible.pixelFormat}); }
|
|
116
|
+
catch (error) { visible.pixels.buffer.transfer(0); throw error; }
|
|
117
|
+
}
|
|
118
|
+
const composited = frame.videoLayer?.id === video.config?.id ? compositeVideo(frame, video.frame, format) : frame;
|
|
119
|
+
let captured, result;
|
|
120
|
+
try {
|
|
121
|
+
captured = captureViewport(composited,viewport.x,viewport.y,viewport.width,viewport.height,clip);
|
|
122
|
+
const takeOwnership = captured.pixels.buffer !== frame.pixels.buffer;
|
|
123
|
+
result = rawFrame(captured.pixels,captured.width,captured.height,format,{takeOwnership,inputFormat:composited.pixelFormat ?? "rgba"});
|
|
124
|
+
return result;
|
|
125
|
+
} finally { releaseCapture(frame, composited, captured, result?.pixels.buffer); }
|
|
92
126
|
});
|
|
93
127
|
}
|
|
94
128
|
const key = `${frameSerial}:${viewport.x}:${viewport.y}:${format}:${quality}:${optimizeForSpeed}:${JSON.stringify(clip)}`;
|
|
95
|
-
return captures.get(key, () => frameLeases.capture(frame => {
|
|
129
|
+
return captures.get(key, () => frameLeases.capture(async frame => {
|
|
96
130
|
const composited = frame.videoLayer?.id === video.config?.id ? compositeVideo(frame, video.frame) : frame;
|
|
97
|
-
|
|
98
|
-
|
|
131
|
+
let captured;
|
|
132
|
+
try {
|
|
133
|
+
captured = captureViewport(composited, viewport.x, viewport.y, viewport.width, viewport.height,clip);
|
|
134
|
+
return await encodeImageBase64(captured.pixels, captured.width, captured.height, { format, quality, optimizeForSpeed });
|
|
135
|
+
} finally { releaseCapture(frame, composited, captured); }
|
|
99
136
|
}));
|
|
100
137
|
},
|
|
101
138
|
scroll(deltaX, deltaY) {
|
|
102
139
|
if (viewport.scrollBy(deltaX, deltaY)) {
|
|
103
140
|
postMessage({ type: "scroll", x: viewport.x, y: viewport.y });
|
|
104
|
-
|
|
141
|
+
notifyFrame();
|
|
105
142
|
}
|
|
106
143
|
return { x: viewport.x, y: viewport.y };
|
|
107
144
|
},
|
|
@@ -128,5 +165,4 @@ async function handleRequest(request) {
|
|
|
128
165
|
// renderer installs seccomp, which forbids opening files.
|
|
129
166
|
await encodeImage(new Uint8Array(4), 1, 1);
|
|
130
167
|
await encodeImage(new Uint8Array(4), 1, 1, { format: "jpeg" });
|
|
131
|
-
await wasmpeg.load();
|
|
132
168
|
postMessage({ type: "ready" });
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { CookieJar } from "../network/cookie-jar.js";
|
|
2
|
+
|
|
3
|
+
/** Shared by the document and its same-origin frames; the controller remains authoritative. */
|
|
4
|
+
export class DocumentCookies {
|
|
5
|
+
#jar;
|
|
6
|
+
#now;
|
|
7
|
+
#send;
|
|
8
|
+
#nextSequence = 1;
|
|
9
|
+
#revision = -1;
|
|
10
|
+
#acknowledged = 0;
|
|
11
|
+
#pending = new Map();
|
|
12
|
+
#legacyCookie = "";
|
|
13
|
+
|
|
14
|
+
constructor(send = () => {}, now = () => Date.now()) {
|
|
15
|
+
this.#send = send;
|
|
16
|
+
this.#now = now;
|
|
17
|
+
this.#jar = new CookieJar({ now });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get(url) {
|
|
21
|
+
return this.#revision < 0 ? this.#legacyCookie : this.#jar.documentCookie(url);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set(url, value) {
|
|
25
|
+
const assignment = String(value);
|
|
26
|
+
// The controller imposes the same IPC limit; rejected writes cannot
|
|
27
|
+
// temporarily create a cookie which the controller will never accept.
|
|
28
|
+
if (assignment.length > 4096) return;
|
|
29
|
+
const sequence = this.#nextSequence++;
|
|
30
|
+
// Legacy embedders need not acknowledge setters. Only retain mutations
|
|
31
|
+
// after the controller has established the versioned snapshot protocol.
|
|
32
|
+
if (this.#revision < 0) {
|
|
33
|
+
this.#legacyCookie = mergeLegacyCookie(this.#legacyCookie, assignment, this.#now());
|
|
34
|
+
} else {
|
|
35
|
+
const mutation = { sequence, url, value: assignment, receivedAt: this.#now() };
|
|
36
|
+
this.#pending.set(sequence, mutation);
|
|
37
|
+
this.#jar.store(url, [assignment], { fromScript: true, receivedAt: mutation.receivedAt });
|
|
38
|
+
}
|
|
39
|
+
this.#send(assignment, url, sequence);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
accept(state) {
|
|
43
|
+
if (!state || !Number.isSafeInteger(state.revision) || state.revision <= this.#revision
|
|
44
|
+
|| !Number.isSafeInteger(state.acknowledged) || state.acknowledged < this.#acknowledged
|
|
45
|
+
|| !Array.isArray(state.cookies)) return false;
|
|
46
|
+
this.#revision = state.revision;
|
|
47
|
+
this.#acknowledged = state.acknowledged;
|
|
48
|
+
this.#legacyCookie = "";
|
|
49
|
+
for (const sequence of this.#pending.keys()) {
|
|
50
|
+
if (sequence <= state.acknowledged) this.#pending.delete(sequence);
|
|
51
|
+
}
|
|
52
|
+
this.#jar.replaceScriptSnapshot(state.cookies);
|
|
53
|
+
// A response may have been queued before a newer synchronous setter ran.
|
|
54
|
+
// Replay only those setters, retaining unrelated server cookies and the
|
|
55
|
+
// original Max-Age start time rather than extending it on each snapshot.
|
|
56
|
+
for (const mutation of this.#pending.values()) {
|
|
57
|
+
this.#jar.store(mutation.url, [mutation.value], { fromScript: true, receivedAt: mutation.receivedAt });
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Compatibility for embedders providing only the old documentCookie string. */
|
|
63
|
+
seed(url, value, replace = false) {
|
|
64
|
+
if (this.#revision >= 0 || typeof value !== "string") return;
|
|
65
|
+
// A header has no Path/Domain metadata, so preserve it verbatim instead of
|
|
66
|
+
// inventing records (which would collapse legitimate duplicate names).
|
|
67
|
+
// An embedded about:blank initial load must not erase its parent's value.
|
|
68
|
+
if (replace || value) this.#legacyCookie = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Preserve the string-only embedder API until a controller snapshot arrives. */
|
|
73
|
+
function mergeLegacyCookie(current, assignment, now) {
|
|
74
|
+
const [pair, ...attributes] = assignment.split(";");
|
|
75
|
+
const equals = pair.indexOf("=");
|
|
76
|
+
if (equals < 1) return current;
|
|
77
|
+
const name = pair.slice(0, equals).trim();
|
|
78
|
+
const value = pair.slice(equals + 1).trim();
|
|
79
|
+
const expired = attributes.some(attribute => {
|
|
80
|
+
const [key, argument = ""] = attribute.split("=").map(part => part.trim().toLowerCase());
|
|
81
|
+
return (key === "max-age" && Number(argument) <= 0)
|
|
82
|
+
|| (key === "expires" && Date.parse(argument) <= now);
|
|
83
|
+
});
|
|
84
|
+
const entries = current ? current.split("; ").filter(entry => !entry.startsWith(`${name}=`)) : [];
|
|
85
|
+
if (!expired) entries.push(`${name}=${value}`);
|
|
86
|
+
return entries.join("; ");
|
|
87
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// every document and iframe.
|
|
4
4
|
const encoder = new TextEncoder();
|
|
5
5
|
const MAX_FONT_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
const MAX_RANKED_FONT_CHOICES = 128;
|
|
6
7
|
|
|
7
8
|
export function registerTypeface(CanvasKit, provider, typeface, family) {
|
|
8
9
|
const bytes = encoder.encode(`${family}\0`);
|
|
@@ -37,7 +38,13 @@ function weightRank(face, wanted) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function covers(face, point) {
|
|
40
|
-
|
|
41
|
+
const ranges = face.unicodeRanges;
|
|
42
|
+
if (!ranges) return 0 <= point && point <= 0x10ffff;
|
|
43
|
+
for (let index = 0; index < ranges.length; index++) {
|
|
44
|
+
const range = ranges[index];
|
|
45
|
+
if (range[0] <= point && point <= range[1]) return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
function stretchRank(face, wanted) {
|
|
@@ -61,20 +68,37 @@ export function neededFontFaces(faces, textRuns, availableFamilies = new Set())
|
|
|
61
68
|
if (!families.has(key)) families.set(key, []);
|
|
62
69
|
families.get(key).push(face);
|
|
63
70
|
}
|
|
71
|
+
return neededFacesFromIndex(families, new Map(), textRuns, availableFamilies);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function orderedFontFaces(families, choices, name, weight, style, stretch) {
|
|
75
|
+
const definitions = families.get(name);
|
|
76
|
+
if (!definitions) return null;
|
|
77
|
+
const key = JSON.stringify([name, weight, style, stretch]);
|
|
78
|
+
let ordered = choices.get(key);
|
|
79
|
+
if (!ordered) {
|
|
80
|
+
// Ranking is independent of Unicode coverage; stable sorting preserves
|
|
81
|
+
// the CSS face order even when a character filters out other subsets.
|
|
82
|
+
ordered = ranked(definitions, weight, style, stretch);
|
|
83
|
+
if (choices.size >= MAX_RANKED_FONT_CHOICES) choices.clear();
|
|
84
|
+
choices.set(key, ordered);
|
|
85
|
+
}
|
|
86
|
+
return ordered;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function neededFacesFromIndex(families, choices, textRuns, availableFamilies) {
|
|
64
90
|
const needed = new Set();
|
|
65
|
-
const choices = new Map();
|
|
66
91
|
for (const { text, style } of textRuns) {
|
|
67
92
|
if (!text || !style) continue;
|
|
68
93
|
const points = new Set([...text].map(character => character.codePointAt(0)));
|
|
69
94
|
for (const family of style.fontFamily ?? []) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
const name = family.toLowerCase();
|
|
96
|
+
const ordered = orderedFontFaces(families, choices, name,
|
|
97
|
+
style.fontWeight ?? 400, style.fontStyle ?? "normal", style.fontStretch ?? 100);
|
|
98
|
+
if (!ordered) {
|
|
99
|
+
if (availableFamilies.has(name)) break;
|
|
73
100
|
continue;
|
|
74
101
|
}
|
|
75
|
-
const key = `${family.toLowerCase()}\n${style.fontWeight ?? 400}/${style.fontStyle ?? "normal"}/${style.fontStretch ?? 100}`;
|
|
76
|
-
let ordered = choices.get(key);
|
|
77
|
-
if (!ordered) choices.set(key, ordered = ranked(definitions, style.fontWeight ?? 400, style.fontStyle ?? "normal", style.fontStretch ?? 100));
|
|
78
102
|
for (const point of points) {
|
|
79
103
|
const face = ordered.find(face => covers(face, point));
|
|
80
104
|
if (face) { needed.add(face); points.delete(point); }
|
|
@@ -94,7 +118,9 @@ export class DocumentFonts {
|
|
|
94
118
|
#error;
|
|
95
119
|
#provider = null;
|
|
96
120
|
#collection = null;
|
|
97
|
-
#
|
|
121
|
+
#families = new Map();
|
|
122
|
+
#faceKeys = new WeakMap();
|
|
123
|
+
#rankedFaces = new Map();
|
|
98
124
|
#records = new Map();
|
|
99
125
|
#sources = new Map();
|
|
100
126
|
#pending = new Set();
|
|
@@ -119,7 +145,16 @@ export class DocumentFonts {
|
|
|
119
145
|
get hasUnicodeRanges() { return this.#hasUnicodeRanges; }
|
|
120
146
|
|
|
121
147
|
setFaces(faces) {
|
|
122
|
-
this.#
|
|
148
|
+
this.#families.clear();
|
|
149
|
+
this.#faceKeys = new WeakMap();
|
|
150
|
+
this.#rankedFaces.clear();
|
|
151
|
+
for (const face of faces) {
|
|
152
|
+
const family = face.family.toLowerCase();
|
|
153
|
+
let definitions = this.#families.get(family);
|
|
154
|
+
if (!definitions) this.#families.set(family, definitions = []);
|
|
155
|
+
definitions.push(face);
|
|
156
|
+
this.#faceKeys.set(face, JSON.stringify(face));
|
|
157
|
+
}
|
|
123
158
|
this.#hasUnicodeRanges = faces.some(face => face.unicodeRanges?.length
|
|
124
159
|
&& !(face.unicodeRanges.length === 1 && face.unicodeRanges[0][0] === 0 && face.unicodeRanges[0][1] === 0x10ffff));
|
|
125
160
|
}
|
|
@@ -127,8 +162,8 @@ export class DocumentFonts {
|
|
|
127
162
|
update(faces, textRuns) {
|
|
128
163
|
if (this.#closed) return;
|
|
129
164
|
this.setFaces(faces);
|
|
130
|
-
for (const face of
|
|
131
|
-
const key =
|
|
165
|
+
for (const face of neededFacesFromIndex(this.#families, this.#rankedFaces, textRuns, this.#availableFamilies)) {
|
|
166
|
+
const key = this.#faceKeys.get(face);
|
|
132
167
|
if (this.#records.has(key)) continue;
|
|
133
168
|
const record = { face, alias: null };
|
|
134
169
|
this.#records.set(key, record);
|
|
@@ -141,9 +176,16 @@ export class DocumentFonts {
|
|
|
141
176
|
|
|
142
177
|
/** Available faces in CSS fallback order; unicode-subset files share a family. */
|
|
143
178
|
resolve(family, weight = 400, style = "normal", point = null, stretch = 100) {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
179
|
+
const ordered = orderedFontFaces(this.#families, this.#rankedFaces, family.toLowerCase(), weight, style, stretch);
|
|
180
|
+
if (!ordered) return [];
|
|
181
|
+
const aliases = [];
|
|
182
|
+
for (const face of ordered) {
|
|
183
|
+
if (point !== null && !covers(face, point)) continue;
|
|
184
|
+
// Read the live record: a face may finish loading after it was ranked.
|
|
185
|
+
const alias = this.#records.get(this.#faceKeys.get(face))?.alias;
|
|
186
|
+
if (alias) aliases.push(alias);
|
|
187
|
+
}
|
|
188
|
+
return aliases;
|
|
147
189
|
}
|
|
148
190
|
|
|
149
191
|
async ready() {
|
|
@@ -222,6 +264,8 @@ export class DocumentFonts {
|
|
|
222
264
|
for (const source of this.#sources.values()) source.then(typeface => typeface?.delete(), () => {});
|
|
223
265
|
this.#sources.clear();
|
|
224
266
|
this.#records.clear();
|
|
225
|
-
this.#
|
|
267
|
+
this.#families.clear();
|
|
268
|
+
this.#faceKeys = new WeakMap();
|
|
269
|
+
this.#rankedFaces.clear();
|
|
226
270
|
}
|
|
227
271
|
}
|
package/lib/renderer/host.js
CHANGED
|
@@ -70,6 +70,8 @@ export class RendererHost {
|
|
|
70
70
|
// The document this host asked its renderer to load: the trusted origin
|
|
71
71
|
// for everything the renderer requests.
|
|
72
72
|
#documentUrl = "about:blank";
|
|
73
|
+
#cookieSequence = 0;
|
|
74
|
+
#cookieRevision = 0;
|
|
73
75
|
#child = null;
|
|
74
76
|
#ready = null;
|
|
75
77
|
#pending = new Map();
|
|
@@ -100,7 +102,7 @@ export class RendererHost {
|
|
|
100
102
|
* onNavigate?: (url: string, downloadName: string | null, fragmentFrom: { x: number, y: number } | null,
|
|
101
103
|
* post: { method: "POST", body: Uint8Array, contentType: string, referrer: string } | null) => void,
|
|
102
104
|
* onDownload?: (payload: { url: string, name: string, data: Uint8Array }) => void,
|
|
103
|
-
* onFrame?: () => void,
|
|
105
|
+
* onFrame?: (frame?: { scrollOffset: { x: number, y: number } }) => void,
|
|
104
106
|
* onLifecycle?: (name: "DOMContentLoaded" | "load", generation: number) => void,
|
|
105
107
|
* compositor?: boolean,
|
|
106
108
|
* fetch?: typeof fetch,
|
|
@@ -108,7 +110,7 @@ export class RendererHost {
|
|
|
108
110
|
* }} options
|
|
109
111
|
*/
|
|
110
112
|
constructor({
|
|
111
|
-
entry = DEFAULT_ENTRY, args = [], timeout =
|
|
113
|
+
entry = DEFAULT_ENTRY, args = [], timeout = 0, onRestart = null, onNavigate = null, onDownload = null,
|
|
112
114
|
fetch: fetchImpl = fetch,
|
|
113
115
|
network = new NetworkService({ fetch: fetchImpl }), onFrame = null, onLifecycle = null,
|
|
114
116
|
// A dedicated loopback socket works independently of the main thread on all platforms.
|
|
@@ -172,13 +174,11 @@ export class RendererHost {
|
|
|
172
174
|
if (message?.type === "ready") {
|
|
173
175
|
this.#sandbox = sanitizeSandboxReport(message.sandbox);
|
|
174
176
|
this.#rejectStartup=null;
|
|
175
|
-
clearTimeout(startupTimer);
|
|
176
177
|
resolveReady();
|
|
177
178
|
}
|
|
178
179
|
else this.#handleMessage(child, message);
|
|
179
180
|
},
|
|
180
181
|
onExit: (_process, exitCode, signalCode) => {
|
|
181
|
-
clearTimeout(startupTimer);
|
|
182
182
|
this.#closeChannel(channel);
|
|
183
183
|
this.#closeSockets(child);
|
|
184
184
|
this.#closeAudio(child);
|
|
@@ -194,9 +194,10 @@ export class RendererHost {
|
|
|
194
194
|
},
|
|
195
195
|
});
|
|
196
196
|
this.#child = child;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
this.#cookieSequence = 0;
|
|
198
|
+
// Startup has no deadline: compiling WASM on a slow machine can take
|
|
199
|
+
// arbitrarily long. Exit and explicit close still reject the wait.
|
|
200
|
+
// Interactive method watchdogs start only after the renderer is ready.
|
|
200
201
|
if (logging) {
|
|
201
202
|
pipeToLog(child.stdout, `renderer ${child.pid}`, (chunk) => process.stdout.write(chunk));
|
|
202
203
|
pipeToLog(child.stderr, `renderer ${child.pid}`, (chunk) => process.stderr.write(chunk));
|
|
@@ -270,16 +271,22 @@ export class RendererHost {
|
|
|
270
271
|
catch(error) { channel.pending.get(id).reject(error); channel.pending.delete(id); }
|
|
271
272
|
});
|
|
272
273
|
if (channel.closed || this.#child !== child) throw Error("Renderer closed during capture");
|
|
273
|
-
const sanitized = this.#captureResult(method, result, params);
|
|
274
|
+
const sanitized = this.#captureResult(method, result, params, true);
|
|
274
275
|
if (method === "scroll") this.#pendingScroll = sanitized;
|
|
275
276
|
return sanitized;
|
|
276
277
|
}
|
|
277
278
|
|
|
278
|
-
#captureResult(method,result,params) {
|
|
279
|
+
#captureResult(method,result,params,ownsPacket=false) {
|
|
279
280
|
if (method === 'screenshot' && ['rgba','bgra'].includes(params[0]?.format)) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
try {
|
|
282
|
+
if (result?.format !== params[0].format) throw Error('Raw screenshot format mismatch');
|
|
283
|
+
this.#rawFrames ??= new RawFrames();
|
|
284
|
+
return this.#rawFrames.write(result.pixels,result.width,result.height,result.format);
|
|
285
|
+
} finally {
|
|
286
|
+
// PacketReader owns a standalone allocation; write() above is sync.
|
|
287
|
+
// Main-thread IPC fallback buffers have a different ownership contract.
|
|
288
|
+
if (ownsPacket && result?.pixels instanceof Uint8Array) result.pixels.buffer.transfer(0);
|
|
289
|
+
}
|
|
283
290
|
}
|
|
284
291
|
return sanitizeCompositorResult(method,result);
|
|
285
292
|
}
|
|
@@ -287,7 +294,12 @@ export class RendererHost {
|
|
|
287
294
|
#handleCompositorMessage(channel, message, pixels) {
|
|
288
295
|
if (channel.closed || typeof message !== "object" || message === null) return;
|
|
289
296
|
if (message.type === "frame") {
|
|
290
|
-
if (this.#channel === channel)
|
|
297
|
+
if (this.#channel === channel) {
|
|
298
|
+
const offset = message.scrollOffset;
|
|
299
|
+
const frame = Number.isFinite(offset?.x) && Number.isFinite(offset?.y)
|
|
300
|
+
? { scrollOffset: { x: offset.x, y: offset.y } } : undefined;
|
|
301
|
+
this.#onFrame?.(frame);
|
|
302
|
+
}
|
|
291
303
|
} else if (Number.isInteger(message.id) && channel.pending.has(message.id)) {
|
|
292
304
|
const pending = channel.pending.get(message.id);
|
|
293
305
|
channel.pending.delete(message.id);
|
|
@@ -329,9 +341,17 @@ export class RendererHost {
|
|
|
329
341
|
const child = this.#child;
|
|
330
342
|
if (method === 'click' || method === 'press') this.#clipboardUntil = Date.now()+5000;
|
|
331
343
|
if (method === 'loadDocument') this.#clipboardUntil = 0;
|
|
344
|
+
if (method === 'loadDocument' && params[0]) {
|
|
345
|
+
params = [{ ...params[0], cookieState: this.#cookieState() }, ...params.slice(1)];
|
|
346
|
+
}
|
|
332
347
|
const id = this.#nextId++;
|
|
333
348
|
return new Promise((resolve, reject) => {
|
|
334
|
-
|
|
349
|
+
// Navigation, including its final fragment positioning, can include
|
|
350
|
+
// expensive layout/script work. Close/exit cancels it, not a time limit.
|
|
351
|
+
const navigating = method === 'loadDocument' || method === 'showFragment';
|
|
352
|
+
// A watchdog is opt-in: elapsed time alone cannot distinguish a slow
|
|
353
|
+
// renderer from a hung one, including title queries during page loading.
|
|
354
|
+
const timer = navigating || !(timeout > 0) ? null : setTimeout(() => {
|
|
335
355
|
this.#pending.delete(id);
|
|
336
356
|
// Watchdog: a renderer that misses its deadline is killed outright.
|
|
337
357
|
this.#child = null;
|
|
@@ -457,7 +477,12 @@ export class RendererHost {
|
|
|
457
477
|
} else if (message.type === "lifecycle" && LIFECYCLE_NAMES.has(message.name) && Number.isInteger(message.generation)) {
|
|
458
478
|
this.#onLifecycle?.(message.name, message.generation);
|
|
459
479
|
} else if (message.type === "cookie" && typeof message.value === "string" && message.value.length <= 4096) {
|
|
460
|
-
|
|
480
|
+
const url = typeof message.url === "string" ? message.url : this.#documentUrl;
|
|
481
|
+
if (sameOrigin(url, this.#documentUrl)) this.#network.setDocumentCookie(url, message.value);
|
|
482
|
+
if (Number.isSafeInteger(message.sequence) && message.sequence > this.#cookieSequence) {
|
|
483
|
+
this.#cookieSequence = message.sequence;
|
|
484
|
+
}
|
|
485
|
+
child.send({ type: "cookie-state", state: this.#cookieState() });
|
|
461
486
|
} else if (message.type === "navigate" && typeof message.url === "string") {
|
|
462
487
|
// A page asked to navigate (location.href = ...): validate before obeying.
|
|
463
488
|
let target;
|
|
@@ -652,7 +677,7 @@ export class RendererHost {
|
|
|
652
677
|
url: event.url || target.href,
|
|
653
678
|
contentType: event.contentType ?? "",
|
|
654
679
|
contentLength: event.contentLength ?? null,
|
|
655
|
-
documentCookie: this.#network.documentCookie(this.#documentUrl),
|
|
680
|
+
documentCookie: this.#network.documentCookie(this.#documentUrl), cookieState: this.#cookieState(),
|
|
656
681
|
},
|
|
657
682
|
});
|
|
658
683
|
} else if (event.type === "chunk" && event.data instanceof Uint8Array) {
|
|
@@ -683,7 +708,7 @@ export class RendererHost {
|
|
|
683
708
|
url: response.url || target.href,
|
|
684
709
|
contentType: response.contentType,
|
|
685
710
|
body: response.body,
|
|
686
|
-
documentCookie: this.#network.documentCookie(this.#documentUrl),
|
|
711
|
+
documentCookie: this.#network.documentCookie(this.#documentUrl), cookieState: this.#cookieState(),
|
|
687
712
|
},
|
|
688
713
|
});
|
|
689
714
|
} catch (error) {
|
|
@@ -711,7 +736,7 @@ export class RendererHost {
|
|
|
711
736
|
if (event.type === "head") {
|
|
712
737
|
streamHead = true;
|
|
713
738
|
child.send({ type: "page-fetch-head", id, result: {
|
|
714
|
-
...event.result, documentCookie: this.#network.documentCookie(this.#documentUrl),
|
|
739
|
+
...event.result, documentCookie: this.#network.documentCookie(this.#documentUrl), cookieState: this.#cookieState(),
|
|
715
740
|
} });
|
|
716
741
|
}
|
|
717
742
|
else if (event.type === "chunk") child.send({ type: "page-fetch-chunk", id, data: event.data });
|
|
@@ -724,7 +749,7 @@ export class RendererHost {
|
|
|
724
749
|
}
|
|
725
750
|
const { rawHeaders, ...result } = response;
|
|
726
751
|
logFetch(sanitized, `${response.status} ${response.body?.byteLength ?? 0}B`, started);
|
|
727
|
-
reply({ result: { ...result, documentCookie: this.#network.documentCookie(this.#documentUrl) } });
|
|
752
|
+
reply({ result: { ...result, documentCookie: this.#network.documentCookie(this.#documentUrl), cookieState: this.#cookieState() } });
|
|
728
753
|
} catch (error) {
|
|
729
754
|
logFetch(sanitized ?? request, `failed: ${error?.message ?? error}`, started);
|
|
730
755
|
if (streamHead && this.#child === child) {
|
|
@@ -734,6 +759,10 @@ export class RendererHost {
|
|
|
734
759
|
if (this.#pageFetches.get(id) === entry) this.#pageFetches.delete(id);
|
|
735
760
|
}
|
|
736
761
|
}
|
|
762
|
+
#cookieState() {
|
|
763
|
+
return { revision: ++this.#cookieRevision, acknowledged: this.#cookieSequence,
|
|
764
|
+
cookies: this.#network.cookies.scriptSnapshot(this.#documentUrl) };
|
|
765
|
+
}
|
|
737
766
|
}
|
|
738
767
|
|
|
739
768
|
/** With BUNINU_LOG, one line per page fetch()/XMLHttpRequest: method, URL (shortened), outcome, time. */
|