buninu-browser 0.5.0 → 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/inert-link.js +26 -0
- package/lib/happy-dom/mutation-observer.js +157 -0
- package/lib/happy-dom/node-iterator-removal.js +36 -0
- package/lib/happy-dom/node-replacement.js +60 -0
- package/lib/happy-dom/parser.js +6 -1
- package/lib/headless-shell.js +49 -9
- package/lib/layout/repaint.js +89 -0
- package/lib/layout/text-layout.js +4 -2
- 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 +107 -3
- 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 +577 -201
- 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/compare-node-iterator.js +42 -0
- 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/fractional-text-scale.js +38 -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/node-iterator-cases.js +59 -0
- package/test/fixtures/node-iterator-chromium.json +315 -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/fractional-text-scale.test.js +6 -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/inert-link.test.js +37 -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/node-iterator.test.js +81 -0
- package/test/observer-invalidation.test.js +12 -0
- package/test/page-scripts.test.js +151 -1
- 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/text-layout.test.js +15 -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
- package/test/window-resize.test.js +29 -0
|
@@ -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. */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cooperative idle work shares a short period, rather than granting every
|
|
3
|
+
* queued callback a fresh budget. Dispatch one callback per host task so
|
|
4
|
+
* ordinary timers and input can run between callbacks. A callback registered
|
|
5
|
+
* during a period joins the next one; positive timeouts have their own task.
|
|
6
|
+
* Clock/timer injection keeps the fairness and lifetime tests deterministic.
|
|
7
|
+
*/
|
|
8
|
+
export function createIdleCallbacks({ now, setTimer, clearTimer, invoke }) {
|
|
9
|
+
const callbacks = new Map();
|
|
10
|
+
let runnable = [], next = 0, deadline = 0, timer = null;
|
|
11
|
+
let running = false, disposed = false;
|
|
12
|
+
|
|
13
|
+
function queue() {
|
|
14
|
+
if (disposed) return;
|
|
15
|
+
if (!callbacks.size) {
|
|
16
|
+
if (timer !== null) clearTimer(timer);
|
|
17
|
+
timer = null;
|
|
18
|
+
runnable = []; next = 0;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (running || timer !== null) return;
|
|
22
|
+
// A nested request must not start a new period before this one ends.
|
|
23
|
+
const delay = next >= runnable.length ? Math.max(1, deadline - now()) : 1;
|
|
24
|
+
timer = setTimer(dispatch, delay);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function remove(record) {
|
|
28
|
+
if (callbacks.get(record.id) !== record) return false;
|
|
29
|
+
callbacks.delete(record.id);
|
|
30
|
+
if (record.timer !== null) clearTimer(record.timer);
|
|
31
|
+
record.timer = null;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function deliver(record, didTimeout, until) {
|
|
36
|
+
if (!remove(record)) return;
|
|
37
|
+
invoke(record.callback, {
|
|
38
|
+
get didTimeout() { return didTimeout; },
|
|
39
|
+
timeRemaining: () => Math.max(0, until - now()),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function queueTimeout(record) {
|
|
44
|
+
// Web IDL permits unsigned-long timeouts, while host timers only accept a
|
|
45
|
+
// signed 32-bit delay. Chunk longer waits instead of overflowing to 1ms.
|
|
46
|
+
record.timer = setTimer(() => {
|
|
47
|
+
record.timer = null;
|
|
48
|
+
if (disposed || callbacks.get(record.id) !== record) return;
|
|
49
|
+
if (now() < record.expires) { queueTimeout(record); return; }
|
|
50
|
+
running = true;
|
|
51
|
+
try { deliver(record, true, now()); }
|
|
52
|
+
finally {
|
|
53
|
+
running = false;
|
|
54
|
+
queue();
|
|
55
|
+
}
|
|
56
|
+
}, Math.min(2147483647, Math.max(1, record.expires - now())));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dispatch() {
|
|
60
|
+
timer = null;
|
|
61
|
+
if (disposed || !callbacks.size) return;
|
|
62
|
+
running = true;
|
|
63
|
+
try {
|
|
64
|
+
const at = now();
|
|
65
|
+
if (at >= deadline) {
|
|
66
|
+
runnable = [...callbacks.values()];
|
|
67
|
+
next = 0;
|
|
68
|
+
deadline = at + 10;
|
|
69
|
+
}
|
|
70
|
+
while (next < runnable.length && callbacks.get(runnable[next].id) !== runnable[next]) next++;
|
|
71
|
+
if (next < runnable.length) {
|
|
72
|
+
const record = runnable[next++], at = now();
|
|
73
|
+
const didTimeout = at >= record.expires;
|
|
74
|
+
deliver(record, didTimeout, didTimeout ? at : deadline);
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
77
|
+
running = false;
|
|
78
|
+
queue();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
request(id, callback, timeout) {
|
|
84
|
+
if (disposed) return id;
|
|
85
|
+
const record = { id, callback, expires: timeout > 0 ? now() + timeout : Infinity, timer: null };
|
|
86
|
+
callbacks.set(id, record);
|
|
87
|
+
if (timeout > 0) queueTimeout(record);
|
|
88
|
+
queue();
|
|
89
|
+
return id;
|
|
90
|
+
},
|
|
91
|
+
cancel(id) {
|
|
92
|
+
const record = callbacks.get(id);
|
|
93
|
+
if (record) remove(record);
|
|
94
|
+
if (!callbacks.size) {
|
|
95
|
+
if (timer !== null) clearTimer(timer);
|
|
96
|
+
timer = null;
|
|
97
|
+
runnable = []; next = 0;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
dispose() {
|
|
101
|
+
disposed = true;
|
|
102
|
+
if (timer !== null) clearTimer(timer);
|
|
103
|
+
timer = null;
|
|
104
|
+
for (const record of callbacks.values()) if (record.timer !== null) clearTimer(record.timer);
|
|
105
|
+
callbacks.clear();
|
|
106
|
+
runnable = []; next = 0;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
package/lib/renderer/main.js
CHANGED
|
@@ -265,7 +265,7 @@ const renderer = await createPageRenderer({
|
|
|
265
265
|
clipboardWriteInput:text=>clipboardWrite(text,'clipboard-write-input'),
|
|
266
266
|
webSockets,
|
|
267
267
|
audio,
|
|
268
|
-
setCookie: (value) => process.send({ type: "cookie", value }),
|
|
268
|
+
setCookie: (value, url, sequence) => process.send({ type: "cookie", value, url, sequence }),
|
|
269
269
|
reportError: (message) => report(`buninu-browser: page error: ${logging ? message : String(message).split("\n")[0]}`),
|
|
270
270
|
consoleMessage: logging ? (level, text) => report(`page console.${level}: ${text}`) : null,
|
|
271
271
|
trace: logging ? (text) => report(`timing: ${text}`) : null,
|
|
@@ -352,6 +352,10 @@ async function warmUp() {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
process.on("message", async (message) => {
|
|
355
|
+
if (message?.type === "cookie-state") {
|
|
356
|
+
renderer.updateCookies(message.state);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
355
359
|
if(message?.type==='clipboard-result') {
|
|
356
360
|
const pending=clipboardWrites.get(message.id);if(!pending)return;
|
|
357
361
|
clipboardWrites.delete(message.id);clearTimeout(pending.timer);
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createBytePipe } from "../media/byte-pipe.js";
|
|
16
16
|
import { createSourceSnapshots } from "../media/source-snapshots.js";
|
|
17
|
+
import { createWebMAudioSnapshots } from "../media/webm-snapshots.js";
|
|
17
18
|
import { createWebMStream, ebmlHeader } from "../media/webm-stream.js";
|
|
18
19
|
|
|
19
20
|
const BRAND = Symbol("MediaSource");
|
|
@@ -444,6 +445,8 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
444
445
|
this._webm = {};
|
|
445
446
|
this._webmCovered = new Map();
|
|
446
447
|
this._webmStream = /^(video|audio)\/webm\b/i.test(this._mime) ? createWebMStream() : null;
|
|
448
|
+
this._webmSnapshots = /^audio\/webm\b/i.test(this._mime) ? createWebMAudioSnapshots() : null;
|
|
449
|
+
if (this._webmSnapshots) this._pipe.snapshotAt = time => this._webmSnapshots.at(time, () => this._pipe.bytes());
|
|
447
450
|
this._hasSegmentTimeline = false;
|
|
448
451
|
this._hasMediaSegment = false;
|
|
449
452
|
this._appendWindowStart = 0;
|
|
@@ -589,6 +592,7 @@ export function createMediaSourceScope({ origin, schedule, DOMException, onAppen
|
|
|
589
592
|
addCovered(this._covered, start, end);
|
|
590
593
|
}
|
|
591
594
|
}
|
|
595
|
+
this._webmSnapshots?.append({ id, bytes, cluster });
|
|
592
596
|
if (cluster && this._webmCluster !== cluster.token) {
|
|
593
597
|
for (const prefix of cluster.prefix) this._pipe.append(prefix);
|
|
594
598
|
this._webmCluster = cluster.token;
|