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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Bun supplies the structured-clone transport. Names are scoped by browser
|
|
2
|
+
// origin; the native channels and their event objects never reach page code.
|
|
3
|
+
const NativeBroadcastChannel = globalThis.BroadcastChannel;
|
|
4
|
+
|
|
5
|
+
export function createBroadcastChannels({ origin, EventTarget, realm, runTask, DOMException }) {
|
|
6
|
+
// Renderer workers have separate module state but share Bun's channel
|
|
7
|
+
// transport, so a counter would collide for unrelated opaque documents.
|
|
8
|
+
const scope = origin === "null" ? `opaque:${crypto.randomUUID()}` : origin;
|
|
9
|
+
const states = new WeakMap();
|
|
10
|
+
const live = new Set();
|
|
11
|
+
let disposed = false;
|
|
12
|
+
const stateOf = object => {
|
|
13
|
+
const state = states.get(object);
|
|
14
|
+
if (!state) throw new TypeError("Illegal invocation");
|
|
15
|
+
return state;
|
|
16
|
+
};
|
|
17
|
+
function BroadcastChannel(name) {
|
|
18
|
+
if (!new.target) throw new TypeError("Constructor BroadcastChannel requires 'new'");
|
|
19
|
+
if (!arguments.length) throw new TypeError("BroadcastChannel requires a name");
|
|
20
|
+
if (disposed) throw new DOMException("The document is no longer active", "InvalidStateError");
|
|
21
|
+
const self = Reflect.construct(EventTarget, [], new.target);
|
|
22
|
+
const channelName = String(name);
|
|
23
|
+
const native = new NativeBroadcastChannel(JSON.stringify([scope, channelName]));
|
|
24
|
+
const state = { native, name: channelName, closed: false, handlers: new Map() };
|
|
25
|
+
states.set(self, state);
|
|
26
|
+
live.add(state);
|
|
27
|
+
for (const type of ["message", "messageerror"]) {
|
|
28
|
+
native.addEventListener(type, incoming => {
|
|
29
|
+
if (disposed || state.closed) return;
|
|
30
|
+
runTask(() => {
|
|
31
|
+
const event = realm.messageEvent(type, { data: incoming.data, origin });
|
|
32
|
+
realm.markTrusted(event);
|
|
33
|
+
EventTarget.prototype.dispatchEvent.call(self, event);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return self;
|
|
38
|
+
}
|
|
39
|
+
BroadcastChannel.prototype = Object.create(EventTarget.prototype, {
|
|
40
|
+
constructor: { value: BroadcastChannel, writable: true, configurable: true },
|
|
41
|
+
name: { get() { return stateOf(this).name; }, enumerable: true, configurable: true },
|
|
42
|
+
postMessage: { value(message) {
|
|
43
|
+
const state = stateOf(this);
|
|
44
|
+
if (state.closed) throw new DOMException("The channel is closed", "InvalidStateError");
|
|
45
|
+
if (!arguments.length) throw new TypeError("postMessage requires a message");
|
|
46
|
+
try { state.native.postMessage(message); }
|
|
47
|
+
catch (error) { throw new DOMException(String(error.message ?? error), error.name === "DataCloneError" ? "DataCloneError" : "InvalidStateError"); }
|
|
48
|
+
}, writable: true, enumerable: true, configurable: true },
|
|
49
|
+
close: { value() {
|
|
50
|
+
const state = stateOf(this);
|
|
51
|
+
if (state.closed) return;
|
|
52
|
+
state.closed = true;
|
|
53
|
+
state.native.close();
|
|
54
|
+
live.delete(state);
|
|
55
|
+
}, writable: true, enumerable: true, configurable: true },
|
|
56
|
+
[Symbol.toStringTag]: { value: "BroadcastChannel", configurable: true },
|
|
57
|
+
});
|
|
58
|
+
for (const type of ["message", "messageerror"]) Object.defineProperty(BroadcastChannel.prototype, `on${type}`, {
|
|
59
|
+
get() { return stateOf(this).handlers.get(type) ?? null; },
|
|
60
|
+
set(handler) {
|
|
61
|
+
const state = stateOf(this);
|
|
62
|
+
const previous = state.handlers.get(type);
|
|
63
|
+
if (previous) EventTarget.prototype.removeEventListener.call(this, type, previous);
|
|
64
|
+
state.handlers.delete(type);
|
|
65
|
+
if (typeof handler === "function") {
|
|
66
|
+
state.handlers.set(type, handler);
|
|
67
|
+
EventTarget.prototype.addEventListener.call(this, type, handler);
|
|
68
|
+
}
|
|
69
|
+
}, enumerable: true, configurable: true,
|
|
70
|
+
});
|
|
71
|
+
return { BroadcastChannel, dispose() {
|
|
72
|
+
disposed = true;
|
|
73
|
+
for (const state of live) { state.closed = true; state.native.close(); }
|
|
74
|
+
live.clear();
|
|
75
|
+
} };
|
|
76
|
+
}
|
|
@@ -22,9 +22,90 @@ export function setCanvasKit(kit) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const surfaces = new WeakMap();
|
|
25
|
+
// A WeakMap does not release the malloc-backed CanvasKit surface. Track
|
|
26
|
+
// resources without retaining DOM nodes or context proxies, so either page
|
|
27
|
+
// teardown or collection of an unused canvas can release them.
|
|
28
|
+
const documentSurfaces = new WeakMap();
|
|
29
|
+
const documentChanges = new WeakMap();
|
|
30
|
+
const collectedContexts = new FinalizationRegistry(releaseSurface);
|
|
31
|
+
const DRAW_METHODS = new Set(["clearRect", "drawImage", "fill", "fillRect", "fillText", "stroke", "strokeRect", "strokeText", "putImageData"]);
|
|
32
|
+
const CONTEXT_METHODS = new Set(("arc arcTo beginPath bezierCurveTo clearRect clip closePath createImageData createLinearGradient createPattern createRadialGradient drawFocusIfNeeded drawImage ellipse fill fillRect fillText getImageData getLineDash getTransform isPointInPath isPointInStroke lineTo measureText moveTo putImageData quadraticCurveTo rect reset resetTransform restore rotate roundRect save scale setLineDash setTransform stroke strokeRect strokeText transform translate").split(" "));
|
|
33
|
+
const CONTEXT_PROPERTIES = new Set(("canvas direction fillStyle filter font fontKerning fontStretch fontVariantCaps globalAlpha globalCompositeOperation imageSmoothingEnabled imageSmoothingQuality letterSpacing lineCap lineDashOffset lineJoin lineWidth miterLimit shadowBlur shadowColor shadowOffsetX shadowOffsetY strokeStyle textAlign textBaseline textRendering wordSpacing").split(" "));
|
|
34
|
+
const nativeStyles = new WeakMap();
|
|
35
|
+
const publicStyles = new WeakMap();
|
|
36
|
+
const collectedStyles = new FinalizationRegistry(native => native.ie());
|
|
37
|
+
|
|
38
|
+
// CanvasKit's gradient/pattern shims expose shader handles and disposal
|
|
39
|
+
// methods. Only the standard author-facing methods may cross into a page.
|
|
40
|
+
function publicStyle(native) {
|
|
41
|
+
if (native === null || typeof native !== "object") return native;
|
|
42
|
+
let wrapper = publicStyles.get(native)?.deref();
|
|
43
|
+
if (wrapper) return wrapper;
|
|
44
|
+
const gradient = typeof native.addColorStop === "function";
|
|
45
|
+
const method = gradient ? "addColorStop" : "setTransform";
|
|
46
|
+
wrapper = Object.create(null);
|
|
47
|
+
Object.defineProperties(wrapper, {
|
|
48
|
+
[method]: { value(...args) {
|
|
49
|
+
// WebIDL converts these before entering the native implementation.
|
|
50
|
+
// The shim otherwise stores the original offset object in its stops,
|
|
51
|
+
// which could retain the facade/DOM through finalizer holdings.
|
|
52
|
+
if (gradient) native.addColorStop(Number(args[0]), String(args[1]));
|
|
53
|
+
else native.setTransform(...args);
|
|
54
|
+
}, writable: true, configurable: true, enumerable: true },
|
|
55
|
+
[Symbol.toStringTag]: { value: gradient ? "CanvasGradient" : "CanvasPattern", configurable: true },
|
|
56
|
+
});
|
|
57
|
+
// A gradient reused after its creator's bitmap reset can own a later
|
|
58
|
+
// shader outside that old context's cleanup list. Its facade owns that
|
|
59
|
+
// last shader; use a weak reverse link so finalizer holdings cannot retain
|
|
60
|
+
// the facade through this map. Context style/save state keeps it alive.
|
|
61
|
+
publicStyles.set(native, new WeakRef(wrapper));
|
|
62
|
+
nativeStyles.set(wrapper, native);
|
|
63
|
+
// The current pattern shim may retain its page-supplied image object;
|
|
64
|
+
// do not root that DOM graph in finalizer holdings. Gradients retain only
|
|
65
|
+
// numeric coordinates/colors and their private native shader.
|
|
66
|
+
if (gradient) {
|
|
67
|
+
collectedStyles.unregister(native);
|
|
68
|
+
collectedStyles.register(wrapper, native, native);
|
|
69
|
+
}
|
|
70
|
+
return wrapper;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Drawing changes paint, without requiring a DOM mutation or full layout. */
|
|
74
|
+
export function observeDocumentCanvases(document, changed) {
|
|
75
|
+
documentChanges.set(document, changed);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function changed(state) {
|
|
79
|
+
state.image?.delete();
|
|
80
|
+
state.image = null;
|
|
81
|
+
state.resource = null;
|
|
82
|
+
const node = state.node.deref();
|
|
83
|
+
if (node) documentChanges.get(node.ownerDocument)?.(node);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function releaseSurface(state) {
|
|
87
|
+
if (!state.surface) return;
|
|
88
|
+
state.image?.delete();
|
|
89
|
+
state.image = state.resource = null;
|
|
90
|
+
state.surface.dispose();
|
|
91
|
+
state.surface = null;
|
|
92
|
+
state.raster = null;
|
|
93
|
+
state.context = null;
|
|
94
|
+
state.owner?.delete(state);
|
|
95
|
+
collectedContexts.unregister(state);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Release connected and detached canvases belonging to a discarded document. */
|
|
99
|
+
export function disposeDocumentCanvases(document) {
|
|
100
|
+
documentChanges.delete(document);
|
|
101
|
+
const owned = documentSurfaces.get(document);
|
|
102
|
+
if (!owned) return;
|
|
103
|
+
for (const state of owned) releaseSurface(state);
|
|
104
|
+
documentSurfaces.delete(document);
|
|
105
|
+
}
|
|
25
106
|
|
|
26
107
|
/** The element's 2d context, or null for any other context id or before CanvasKit loads. */
|
|
27
|
-
export function canvasContext(node, type) {
|
|
108
|
+
export function canvasContext(node, type, options = null) {
|
|
28
109
|
if (type !== "2d" || !CanvasKit) return null;
|
|
29
110
|
let entry = surfaces.get(node);
|
|
30
111
|
if (!entry) {
|
|
@@ -32,6 +113,7 @@ export function canvasContext(node, type) {
|
|
|
32
113
|
if (!entry) return null;
|
|
33
114
|
surfaces.set(node, entry);
|
|
34
115
|
}
|
|
116
|
+
if (options) entry.configure(options);
|
|
35
117
|
return entry.proxy;
|
|
36
118
|
}
|
|
37
119
|
|
|
@@ -40,6 +122,42 @@ export function resetCanvas(node) {
|
|
|
40
122
|
surfaces.get(node)?.reopen();
|
|
41
123
|
}
|
|
42
124
|
|
|
125
|
+
/** A refcounted native snapshot: no PNG/base64/readPixels round trip. */
|
|
126
|
+
export function canvasResource(node) {
|
|
127
|
+
const state = surfaces.get(node)?.state;
|
|
128
|
+
if (!state?.surface || !state.raster) return null;
|
|
129
|
+
if (!state.resource) {
|
|
130
|
+
state.raster.flush();
|
|
131
|
+
state.image = state.raster.makeImageSnapshot();
|
|
132
|
+
if (!state.image) return null;
|
|
133
|
+
state.resource = { image: state.image, width: state.width, height: state.height, originClean: state.originClean };
|
|
134
|
+
}
|
|
135
|
+
return state.resource;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Encoded bytes are only needed when the page explicitly exports its canvas. */
|
|
139
|
+
export function encodeCanvas(node, requestedType = "image/png", quality) {
|
|
140
|
+
if (!node.width || !node.height) return null;
|
|
141
|
+
canvasContext(node, "2d");
|
|
142
|
+
const state = surfaces.get(node).state;
|
|
143
|
+
if (!state.originClean) throw new DOMException("The canvas contains cross-origin image data.", "SecurityError");
|
|
144
|
+
const type = String(requestedType).toLowerCase();
|
|
145
|
+
const format = type === "image/jpeg" ? CanvasKit.ImageFormat.JPEG
|
|
146
|
+
: type === "image/webp" ? CanvasKit.ImageFormat.WEBP : CanvasKit.ImageFormat.PNG;
|
|
147
|
+
let supportedType = format === CanvasKit.ImageFormat.JPEG ? "image/jpeg"
|
|
148
|
+
: format === CanvasKit.ImageFormat.WEBP ? "image/webp" : "image/png";
|
|
149
|
+
const image = canvasResource(node)?.image;
|
|
150
|
+
const level = typeof quality === "number" && quality >= 0 && quality <= 1 ? Math.round(quality * 100) : 92;
|
|
151
|
+
let bytes = image?.encodeToBytes(format ?? CanvasKit.ImageFormat.PNG, level);
|
|
152
|
+
// CanvasKit builds may omit optional encoders. HTML requires the PNG
|
|
153
|
+
// fallback for unsupported types rather than a mislabeled or empty result.
|
|
154
|
+
if (!bytes && format !== CanvasKit.ImageFormat.PNG) {
|
|
155
|
+
bytes = image?.encodeToBytes(CanvasKit.ImageFormat.PNG, level);
|
|
156
|
+
supportedType = "image/png";
|
|
157
|
+
}
|
|
158
|
+
return bytes ? { bytes, type: supportedType } : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
43
161
|
// A zero bitmap makes MakeSurface fail and getImageData reject the rectangle.
|
|
44
162
|
// The element can still report 0; drawing uses one pixel until it grows.
|
|
45
163
|
function bitmapSize(node) {
|
|
@@ -47,32 +165,143 @@ function bitmapSize(node) {
|
|
|
47
165
|
}
|
|
48
166
|
|
|
49
167
|
function open(node) {
|
|
50
|
-
|
|
168
|
+
const surface = makeSurface(node);
|
|
51
169
|
if (!surface) return null;
|
|
52
|
-
let
|
|
170
|
+
let owner = null;
|
|
171
|
+
if (node.ownerDocument) {
|
|
172
|
+
owner = documentSurfaces.get(node.ownerDocument);
|
|
173
|
+
if (!owner) documentSurfaces.set(node.ownerDocument, owner = new Set());
|
|
174
|
+
}
|
|
175
|
+
const [width, height] = bitmapSize(node);
|
|
176
|
+
const state = { surface, raster: nativeSurface(surface), context: surface.getContext("2d"), owner,
|
|
177
|
+
node: new WeakRef(node), width, height, image: null, resource: null, originClean: true };
|
|
178
|
+
owner?.add(state);
|
|
179
|
+
const methods = new Map();
|
|
180
|
+
let publicCanvas = null, resolveImage = null, parseFont = null, text = null;
|
|
181
|
+
const textDefaults = () => ({ font: "10px sans-serif", parsedFont: { fontSize: 10, fontFamily: ["sans-serif"], fontWeight: 400, fontStyle: "normal", fontStretch: 100 },
|
|
182
|
+
textAlign: "start", textBaseline: "alphabetic", direction: "inherit" });
|
|
183
|
+
let settings = textDefaults();
|
|
184
|
+
const settingsStack = [];
|
|
185
|
+
let styleValues = { fillStyle: null, strokeStyle: null };
|
|
186
|
+
const styleStack = [];
|
|
187
|
+
const currentContext = () => {
|
|
188
|
+
if (!state.context) throw new DOMException("The canvas document has been discarded.", "InvalidStateError");
|
|
189
|
+
return state.context;
|
|
190
|
+
};
|
|
53
191
|
const proxy = new Proxy(Object.create(null), {
|
|
54
192
|
get(_target, property) {
|
|
55
|
-
if (property === "
|
|
193
|
+
if (property === "canvas") return publicCanvas;
|
|
194
|
+
if (!CONTEXT_METHODS.has(property) && !CONTEXT_PROPERTIES.has(property)) return undefined;
|
|
195
|
+
const context = currentContext();
|
|
196
|
+
if (text && Object.hasOwn(settings, property) && property !== "parsedFont") return settings[property];
|
|
197
|
+
// The shim's strokeStyle getter assumes every value is a color, even
|
|
198
|
+
// after accepting a gradient. Keep the page's branded style identity.
|
|
199
|
+
if ((property === "fillStyle" || property === "strokeStyle") && styleValues[property]) return styleValues[property];
|
|
56
200
|
const value = context[property];
|
|
57
|
-
|
|
201
|
+
if (property === "fillStyle" || property === "strokeStyle") return publicStyle(value);
|
|
202
|
+
const custom = property === "getTransform";
|
|
203
|
+
if (typeof value !== "function" && !custom) return value;
|
|
204
|
+
let method = methods.get(property);
|
|
205
|
+
if (!method) {
|
|
206
|
+
// A saved method follows width/height resets too. Binding the proxy
|
|
207
|
+
// keeps its resource alive while a caller retains only the method.
|
|
208
|
+
method = function (...args) {
|
|
209
|
+
const context = currentContext();
|
|
210
|
+
if (property === "getTransform") {
|
|
211
|
+
const matrix = state.raster.getCanvas().getTotalMatrix();
|
|
212
|
+
return { a: matrix[0], b: matrix[3], c: matrix[1], d: matrix[4], e: matrix[2], f: matrix[5], is2D: true };
|
|
213
|
+
}
|
|
214
|
+
if (text && ["fillText", "strokeText", "measureText"].includes(property)) {
|
|
215
|
+
const result = text({ operation: property, args, context, canvas: state.raster.getCanvas(), settings,
|
|
216
|
+
paintStyle: nativeStyles.get(styleValues[property === "strokeText" ? "strokeStyle" : "fillStyle"]),
|
|
217
|
+
});
|
|
218
|
+
if (property !== "measureText") changed(state);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
if (property === "getImageData" && !state.originClean) {
|
|
222
|
+
throw new DOMException("The canvas contains cross-origin image data.", "SecurityError");
|
|
223
|
+
}
|
|
224
|
+
let source;
|
|
225
|
+
if (property === "drawImage" && resolveImage) {
|
|
226
|
+
source = resolveImage(args[0]);
|
|
227
|
+
if (!source?.image) return;
|
|
228
|
+
args[0] = source.image;
|
|
229
|
+
}
|
|
230
|
+
const result = context[property](...args);
|
|
231
|
+
if (property === "save") {
|
|
232
|
+
settingsStack.push({ ...settings });
|
|
233
|
+
styleStack.push({ ...styleValues });
|
|
234
|
+
} else if (property === "restore" && settingsStack.length) {
|
|
235
|
+
settings = settingsStack.pop();
|
|
236
|
+
styleValues = styleStack.pop();
|
|
237
|
+
// Native save() copies style shims. The DOM retains the actual
|
|
238
|
+
// gradient/pattern object, including later color-stop changes.
|
|
239
|
+
for (const name of ["fillStyle", "strokeStyle"]) if (styleValues[name]) context[name] = nativeStyles.get(styleValues[name]);
|
|
240
|
+
}
|
|
241
|
+
if (source?.originClean === false) state.originClean = false;
|
|
242
|
+
if (DRAW_METHODS.has(property)) changed(state);
|
|
243
|
+
if (property === "createLinearGradient" || property === "createRadialGradient" || property === "createPattern") return publicStyle(result);
|
|
244
|
+
return property === "getImageData" ? copyImageData(result) : result;
|
|
245
|
+
}.bind(proxy);
|
|
246
|
+
methods.set(property, method);
|
|
247
|
+
}
|
|
248
|
+
return method;
|
|
58
249
|
},
|
|
59
250
|
set(_target, property, value) {
|
|
60
|
-
|
|
251
|
+
if (!CONTEXT_PROPERTIES.has(property) || property === "canvas") return false;
|
|
252
|
+
if (property === "fillStyle" || property === "strokeStyle") {
|
|
253
|
+
const native = value !== null && typeof value === "object" ? nativeStyles.get(value) : undefined;
|
|
254
|
+
// Do not pass arbitrary page objects to the shim's private pe()
|
|
255
|
+
// shader protocol. Only strings or our branded wrappers are styles.
|
|
256
|
+
if (native || typeof value === "string") {
|
|
257
|
+
currentContext()[property] = native ?? value;
|
|
258
|
+
styleValues[property] = native ? value : null;
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
if (text && property === "font") {
|
|
263
|
+
const parsed = parseFont?.(String(value));
|
|
264
|
+
if (parsed) { settings.font = String(value); settings.parsedFont = parsed; }
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
if (text && Object.hasOwn(settings, property)) {
|
|
268
|
+
const values = { textAlign: ["start", "end", "left", "right", "center"], textBaseline: ["top", "hanging", "middle", "alphabetic", "ideographic", "bottom"], direction: ["inherit", "ltr", "rtl"] };
|
|
269
|
+
if (values[property]?.includes(String(value))) settings[property] = String(value);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
currentContext()[property] = value;
|
|
61
273
|
return true;
|
|
62
274
|
},
|
|
63
275
|
has(_target, property) {
|
|
64
|
-
return property in
|
|
276
|
+
return property === "canvas" || property === "getTransform" || (CONTEXT_METHODS.has(property) || CONTEXT_PROPERTIES.has(property)) && (property in currentContext() || text && Object.hasOwn(settings, property));
|
|
65
277
|
},
|
|
66
278
|
});
|
|
279
|
+
collectedContexts.register(proxy, state, state);
|
|
67
280
|
return {
|
|
281
|
+
state,
|
|
68
282
|
proxy,
|
|
283
|
+
configure(options) {
|
|
284
|
+
publicCanvas = options.canvas;
|
|
285
|
+
resolveImage = options.resolveImage;
|
|
286
|
+
parseFont = options.parseFont;
|
|
287
|
+
text = options.text;
|
|
288
|
+
},
|
|
69
289
|
reopen() {
|
|
290
|
+
if (!state.surface) return;
|
|
70
291
|
const next = makeSurface(node);
|
|
71
292
|
if (!next) return;
|
|
72
|
-
const previous = surface;
|
|
73
|
-
surface = next;
|
|
74
|
-
|
|
75
|
-
|
|
293
|
+
const previous = state.surface;
|
|
294
|
+
state.surface = next;
|
|
295
|
+
state.raster = nativeSurface(next);
|
|
296
|
+
state.context = next.getContext("2d");
|
|
297
|
+
state.originClean = true;
|
|
298
|
+
settings = textDefaults();
|
|
299
|
+
settingsStack.length = 0;
|
|
300
|
+
styleValues = { fillStyle: null, strokeStyle: null };
|
|
301
|
+
styleStack.length = 0;
|
|
302
|
+
[state.width, state.height] = bitmapSize(node);
|
|
303
|
+
changed(state);
|
|
304
|
+
previous.dispose();
|
|
76
305
|
},
|
|
77
306
|
};
|
|
78
307
|
}
|
|
@@ -82,11 +311,25 @@ function makeSurface(node) {
|
|
|
82
311
|
return CanvasKit.MakeCanvas(width, height);
|
|
83
312
|
}
|
|
84
313
|
|
|
314
|
+
function nativeSurface(canvas) {
|
|
315
|
+
// The bundled CanvasKit canvas shim owns its SkSurface in an enumerable
|
|
316
|
+
// field, renamed by minification. Use its native API instead of depending
|
|
317
|
+
// on that generated field name or encoding/decoding a canvas data URL.
|
|
318
|
+
return Object.values(canvas).find(value => value && typeof value.makeImageSnapshot === "function"
|
|
319
|
+
&& typeof value.getCanvas === "function") ?? null;
|
|
320
|
+
}
|
|
321
|
+
|
|
85
322
|
function copyImageData(image) {
|
|
86
323
|
if (!image?.data) throw new DOMException("The source rectangle is empty.", "IndexSizeError");
|
|
87
324
|
const count = image.width * image.height * 4;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
325
|
+
// Current CanvasKit returns a fresh, exact-sized copy from readPixels.
|
|
326
|
+
// Keep that array; only copy when the backing buffer exposes other pixels
|
|
327
|
+
// (or the WASM heap), as some CanvasKit versions do.
|
|
328
|
+
let data = image.data;
|
|
329
|
+
if (!(data instanceof Uint8ClampedArray) || data.byteOffset !== 0 || data.buffer.byteLength !== count || data.length !== count) {
|
|
330
|
+
data = new Uint8ClampedArray(count);
|
|
331
|
+
const source = new Uint8ClampedArray(image.data.buffer, image.data.byteOffset, Math.min(count, image.data.length));
|
|
332
|
+
data.set(source);
|
|
333
|
+
}
|
|
91
334
|
return { width: image.width, height: image.height, data, colorSpace: "srgb" };
|
|
92
335
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { parseFontShorthand } from "../style/computed-style.js";
|
|
2
|
+
|
|
3
|
+
const STRETCH = { "ultra-condensed": 50, "extra-condensed": 62.5, condensed: 75, "semi-condensed": 87.5,
|
|
4
|
+
normal: 100, "semi-expanded": 112.5, expanded: 125, "extra-expanded": 150, "ultra-expanded": 200 };
|
|
5
|
+
const SIZES = { "xx-small": 9, "x-small": 10, small: 13, medium: 16, large: 18, "x-large": 24, "xx-large": 32, "xxx-large": 48 };
|
|
6
|
+
|
|
7
|
+
export function parseCanvasFont(value, base = 16, root = 16) {
|
|
8
|
+
const font = parseFontShorthand(String(value));
|
|
9
|
+
if (!font) return null;
|
|
10
|
+
const size = font.size.toLowerCase();
|
|
11
|
+
const length = size.match(/^(\d*\.?\d+)(px|pt|pc|in|cm|mm|q|em|rem|ex|ch|%)$/);
|
|
12
|
+
const factors = { px: 1, pt: 4 / 3, pc: 16, in: 96, cm: 96 / 2.54, mm: 96 / 25.4, q: 96 / 101.6,
|
|
13
|
+
em: base, rem: root, ex: base / 2, ch: base / 2, "%": base / 100 };
|
|
14
|
+
const fontSize = SIZES[size] ?? (size === "larger" ? base * 1.2 : size === "smaller" ? base / 1.2
|
|
15
|
+
: length ? Number(length[1]) * factors[length[2]] : NaN);
|
|
16
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0) return null;
|
|
17
|
+
const fontFamily = font.family.match(/(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|[^,])+/g)?.map(family => family.trim().replace(/^(?:"(.*)"|'(.*)')$/, "$1$2"));
|
|
18
|
+
if (!fontFamily?.length || fontFamily.some(family => !family)) return null;
|
|
19
|
+
return { fontSize, fontFamily, fontWeight: font.weight === "bold" ? 700 : Number(font.weight) || 400,
|
|
20
|
+
fontStyle: font.style, fontStretch: STRETCH[font.stretch] ?? 100 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Draw with the same shaped fonts, fallbacks and Unicode handling as DOM text. */
|
|
24
|
+
export function createCanvasTextRenderer(CanvasKit, makeParagraph) {
|
|
25
|
+
const paragraphs = new Map();
|
|
26
|
+
let characters = 0;
|
|
27
|
+
const render = request => canvasText(CanvasKit, makeParagraph, request, {
|
|
28
|
+
get(key) {
|
|
29
|
+
const entry = paragraphs.get(key);
|
|
30
|
+
if (entry) { paragraphs.delete(key); paragraphs.set(key, entry); }
|
|
31
|
+
return entry?.paragraph;
|
|
32
|
+
},
|
|
33
|
+
set(key, paragraph, count) {
|
|
34
|
+
paragraphs.set(key, { paragraph, count });
|
|
35
|
+
characters += count;
|
|
36
|
+
while (paragraphs.size > 128 || characters > 16000) {
|
|
37
|
+
const [old, entry] = paragraphs.entries().next().value;
|
|
38
|
+
entry.paragraph.delete(); characters -= entry.count; paragraphs.delete(old);
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
render.clear = () => {
|
|
43
|
+
for (const { paragraph } of paragraphs.values()) paragraph.delete();
|
|
44
|
+
paragraphs.clear(); characters = 0;
|
|
45
|
+
};
|
|
46
|
+
return render;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function canvasText(CanvasKit, makeParagraph, { context, canvas, settings, style, operation, args, paintStyle }, cache) {
|
|
50
|
+
const text = String(args[0]).replace(/[\t\n\f\r]/g, " ");
|
|
51
|
+
const stroke = operation === "strokeText";
|
|
52
|
+
const measure = operation === "measureText";
|
|
53
|
+
// CanvasKit's strokeStyle getter serializes every style as a color. The
|
|
54
|
+
// context facade supplies its already-unwrapped gradient/pattern directly.
|
|
55
|
+
const color = paintStyle ?? (stroke ? context.strokeStyle : context.fillStyle);
|
|
56
|
+
const retained = text.length <= 4096 && (measure || typeof color === "string");
|
|
57
|
+
const key = retained ? JSON.stringify([text, style, measure ? "measure" : [stroke, color, context.globalAlpha,
|
|
58
|
+
context.globalCompositeOperation, context.lineWidth, context.miterLimit, context.lineJoin]]) : null;
|
|
59
|
+
let paint, paragraph = retained ? cache.get(key) : null;
|
|
60
|
+
let owned = !paragraph;
|
|
61
|
+
try {
|
|
62
|
+
if (!paragraph && !measure) {
|
|
63
|
+
paint = new CanvasKit.Paint();
|
|
64
|
+
paint.setAntiAlias(true);
|
|
65
|
+
if (typeof color === "string") {
|
|
66
|
+
const rgba = CanvasKit.parseColorString(color);
|
|
67
|
+
rgba[3] *= context.globalAlpha;
|
|
68
|
+
paint.setColor(rgba);
|
|
69
|
+
} else if (typeof color?.pe === "function") {
|
|
70
|
+
paint.setShader(color.pe(canvas.getTotalMatrix()));
|
|
71
|
+
paint.setColor(CanvasKit.Color(0, 0, 0, context.globalAlpha));
|
|
72
|
+
}
|
|
73
|
+
if (stroke) {
|
|
74
|
+
paint.setStyle(CanvasKit.PaintStyle.Stroke);
|
|
75
|
+
paint.setStrokeWidth(context.lineWidth);
|
|
76
|
+
paint.setStrokeMiter(context.miterLimit);
|
|
77
|
+
paint.setStrokeJoin({ round: CanvasKit.StrokeJoin.Round, bevel: CanvasKit.StrokeJoin.Bevel }[context.lineJoin] ?? CanvasKit.StrokeJoin.Miter);
|
|
78
|
+
}
|
|
79
|
+
const blends = { "source-over": "SrcOver", "source-in": "SrcIn", "source-out": "SrcOut", "source-atop": "SrcATop",
|
|
80
|
+
"destination-over": "DstOver", "destination-in": "DstIn", "destination-out": "DstOut", "destination-atop": "DstATop",
|
|
81
|
+
lighter: "Plus", copy: "Src", xor: "Xor", multiply: "Multiply", screen: "Screen", overlay: "Overlay", darken: "Darken",
|
|
82
|
+
lighten: "Lighten", "color-dodge": "ColorDodge", "color-burn": "ColorBurn", "hard-light": "HardLight", "soft-light": "SoftLight",
|
|
83
|
+
difference: "Difference", exclusion: "Exclusion", hue: "Hue", saturation: "Saturation", color: "Color", luminosity: "Luminosity" };
|
|
84
|
+
paint.setBlendMode(CanvasKit.BlendMode[blends[context.globalCompositeOperation] ?? "SrcOver"]);
|
|
85
|
+
}
|
|
86
|
+
if (!paragraph) {
|
|
87
|
+
paragraph = makeParagraph(text, style, undefined, 1, paint);
|
|
88
|
+
paragraph.layout(1e7);
|
|
89
|
+
if (retained) { cache.set(key, paragraph, text.length); owned = false; }
|
|
90
|
+
}
|
|
91
|
+
const width = paragraph.getMaxIntrinsicWidth();
|
|
92
|
+
const line = paragraph.getLineMetrics()[0];
|
|
93
|
+
const alphabetic = paragraph.getAlphabeticBaseline();
|
|
94
|
+
const ascent = line?.ascent ?? alphabetic, descent = line?.descent ?? paragraph.getHeight() - alphabetic;
|
|
95
|
+
const baseline = { top: 0, hanging: alphabetic - ascent * 0.8, middle: (alphabetic - ascent + alphabetic + descent) / 2,
|
|
96
|
+
alphabetic, ideographic: paragraph.getIdeographicBaseline(), bottom: alphabetic + descent }[settings.textBaseline] ?? alphabetic;
|
|
97
|
+
let align = settings.textAlign;
|
|
98
|
+
if (align === "start") align = settings.direction === "rtl" ? "right" : "left";
|
|
99
|
+
if (align === "end") align = settings.direction === "rtl" ? "left" : "right";
|
|
100
|
+
const offset = align === "center" ? width / 2 : align === "right" ? width : 0;
|
|
101
|
+
if (measure) return { width, actualBoundingBoxLeft: offset, actualBoundingBoxRight: width - offset,
|
|
102
|
+
actualBoundingBoxAscent: baseline - (alphabetic - ascent), actualBoundingBoxDescent: alphabetic + descent - baseline,
|
|
103
|
+
fontBoundingBoxAscent: baseline, fontBoundingBoxDescent: paragraph.getHeight() - baseline,
|
|
104
|
+
emHeightAscent: ascent, emHeightDescent: descent, hangingBaseline: alphabetic - ascent * 0.8,
|
|
105
|
+
alphabeticBaseline: alphabetic - baseline, ideographicBaseline: paragraph.getIdeographicBaseline() - baseline };
|
|
106
|
+
const [x, y] = args.slice(1, 3).map(Number);
|
|
107
|
+
const maxWidth = args.length > 3 ? Number(args[3]) : Infinity;
|
|
108
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !(maxWidth > 0) || !text) return;
|
|
109
|
+
const scale = width > maxWidth ? maxWidth / width : 1;
|
|
110
|
+
canvas.save();
|
|
111
|
+
try {
|
|
112
|
+
canvas.translate(x - offset * scale, y - baseline);
|
|
113
|
+
canvas.scale(scale, 1);
|
|
114
|
+
canvas.drawParagraph(paragraph, 0, 0);
|
|
115
|
+
} finally { canvas.restore(); }
|
|
116
|
+
} finally {
|
|
117
|
+
if (owned) paragraph?.delete();
|
|
118
|
+
paint?.delete();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Private loopback transport, connected before renderer seccomp. No inherited
|
|
2
|
+
// numeric descriptors: node:net also supports Windows sockets.
|
|
3
|
+
import {createServer, createConnection} from 'node:net';
|
|
4
|
+
import {randomBytes} from 'node:crypto';
|
|
5
|
+
const MAX_JSON = 96 * 1024 * 1024;
|
|
6
|
+
const MAX_PIXELS = 64 * 1024 * 1024;
|
|
7
|
+
const MAX_QUEUE = 128 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// Length-prefixed JSON followed by optional binary pixels. Copy once into an
|
|
10
|
+
// owned packet so a raster lease may be released before the socket drains.
|
|
11
|
+
export function encodePacket(message, pixels = null) {
|
|
12
|
+
const json = Buffer.from(JSON.stringify(message));
|
|
13
|
+
const size = pixels?.byteLength ?? 0;
|
|
14
|
+
if (json.length > MAX_JSON || size > MAX_PIXELS || json.length + size > MAX_QUEUE) throw Error('Compositor packet too large');
|
|
15
|
+
const packet = Buffer.allocUnsafe(8 + json.length + size);
|
|
16
|
+
packet.writeUInt32LE(json.length, 0); packet.writeUInt32LE(size, 4);
|
|
17
|
+
packet.set(json, 8); if (size) packet.set(pixels, 8 + json.length);
|
|
18
|
+
return packet;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class PacketReader {
|
|
22
|
+
constructor(receive, limit = MAX_QUEUE) { this.receive = receive; this.limit = limit; this.header = Buffer.alloc(8); this.used = 0; this.packet = null; }
|
|
23
|
+
push(chunk) {
|
|
24
|
+
let offset = 0;
|
|
25
|
+
while (offset < chunk.length) {
|
|
26
|
+
const target = this.packet ?? this.header;
|
|
27
|
+
const count = Math.min(target.length - this.used, chunk.length - offset);
|
|
28
|
+
target.set(chunk.subarray(offset, offset + count), this.used);
|
|
29
|
+
this.used += count; offset += count;
|
|
30
|
+
if (this.used !== target.length) continue;
|
|
31
|
+
this.used = 0;
|
|
32
|
+
if (!this.packet) {
|
|
33
|
+
this.jsonLength = this.header.readUInt32LE(0);
|
|
34
|
+
const binaryLength = this.header.readUInt32LE(4);
|
|
35
|
+
const total = this.jsonLength + binaryLength;
|
|
36
|
+
if (!this.jsonLength || this.jsonLength > MAX_JSON || binaryLength > MAX_PIXELS || total > this.limit) throw Error('Invalid compositor packet size');
|
|
37
|
+
this.packet = Buffer.allocUnsafe(total);
|
|
38
|
+
} else {
|
|
39
|
+
const packet = this.packet; this.packet = null;
|
|
40
|
+
const message = JSON.parse(packet.subarray(0, this.jsonLength).toString());
|
|
41
|
+
const pixels = packet.subarray(this.jsonLength);
|
|
42
|
+
this.receive(message, pixels);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function attach(socket, receive, closed, limit) {
|
|
49
|
+
socket.setNoDelay(true);
|
|
50
|
+
const reader = new PacketReader(receive, limit);
|
|
51
|
+
socket.on('data', chunk => { try { reader.push(chunk); } catch { socket.destroy(); } });
|
|
52
|
+
socket.on('error', () => {});
|
|
53
|
+
socket.once('close', closed);
|
|
54
|
+
return {
|
|
55
|
+
reader,
|
|
56
|
+
send(message, pixels) {
|
|
57
|
+
if (socket.destroyed) throw Error('Compositor channel closed');
|
|
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(); },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function listenCompositor(receive, closed) {
|
|
67
|
+
const token = randomBytes(32).toString('hex');
|
|
68
|
+
const sockets = new Set(); let peer = null, ended = false;
|
|
69
|
+
const server = createServer(socket => {
|
|
70
|
+
if (ended || peer || sockets.size >= 4) { socket.destroy(); return; }
|
|
71
|
+
sockets.add(socket);
|
|
72
|
+
const timer = setTimeout(() => socket.destroy(), 3000);
|
|
73
|
+
const candidate = attach(socket, (message, pixels) => {
|
|
74
|
+
if (!peer) {
|
|
75
|
+
if (pixels.length || message?.type !== 'hello' || message.token !== token) { socket.destroy(); return; }
|
|
76
|
+
peer = candidate; clearTimeout(timer); candidate.reader.limit = MAX_QUEUE;
|
|
77
|
+
for (const other of sockets) if (other !== socket) other.destroy();
|
|
78
|
+
server.close(); candidate.send({type:'accepted'});
|
|
79
|
+
} else if (peer === candidate) receive(message, pixels);
|
|
80
|
+
}, () => { clearTimeout(timer); sockets.delete(socket); if (peer === candidate && !ended) closed(); }, 4096);
|
|
81
|
+
});
|
|
82
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
83
|
+
return {
|
|
84
|
+
endpoint: {port:server.address().port, token},
|
|
85
|
+
send(message) { if (!peer || ended) throw Error('Compositor not connected'); peer.send(message); },
|
|
86
|
+
close() { ended = true; server.close(); for (const socket of sockets) socket.destroy(); },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function connectCompositor(endpoint, receive, closed) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const socket = createConnection({host:'127.0.0.1', port:endpoint.port});
|
|
93
|
+
let accepted = false;
|
|
94
|
+
const timer = setTimeout(() => socket.destroy(), 5000);
|
|
95
|
+
const peer = attach(socket, (message, pixels) => {
|
|
96
|
+
if (!accepted) {
|
|
97
|
+
if (message?.type !== 'accepted' || pixels.length) { socket.destroy(); return; }
|
|
98
|
+
accepted = true; clearTimeout(timer); resolve(peer);
|
|
99
|
+
} else receive(message, pixels);
|
|
100
|
+
}, () => { clearTimeout(timer); if (!accepted) reject(Error('Compositor connection failed')); else closed(); });
|
|
101
|
+
socket.once('connect', () => peer.send({type:'hello', token:endpoint.token}));
|
|
102
|
+
});
|
|
103
|
+
}
|