21 KiB
11 — Pitfalls, Anti-patterns, and What to Avoid
This is the prominent pitfalls chapter. Greenfield's known weaknesses, anti-patterns, and design pitfalls are catalogued here so the user can avoid making the same mistakes in their own Wayland-to-HTML compositor (Odoo 20 + JS/Python). Each pitfall comes with: what Greenfield does (with file/directory reference), why it's a problem (technical reason), and what the user should do instead (concrete recommendation for their JS/Python + Odoo 20 stack).
Headline numbers
- 299
TODO|FIXME|HACK|XXX|@deprecated|workaroundhits across non-vendored source (after excludingexamples/sdk/*upstreams andxtsb/protocol/xproto.xml). - 13 packages all pinned to
"version": "1.0.0-rc1"— noCHANGELOG.md, noSECURITY.md, noCONTRIBUTING.md, no issue/PR templates. - 1 commit visible on
master(6c578f4, "Fix typo in code snippet (#161)", 2025-10-15) — implying the public repo is a release snapshot, not the live dev branch. - 3 native Node
.nodeaddons, all Linux-x64 only. - 0 uses of
OffscreenCanvasortransferControlToOffscreenanywhere in the compositor. - 2 Web Workers used — and only for the WASM H.264 fallback decoder path. The primary WebCodecs path uses none.
Catalogued pitfalls (A–U)
Critical (will bite you on day 1)
A. Native Node addons
What Greenfield does: Three Node-API addons:
wayland-server-addon.node(1231 lines C, wraps patched libwayland-server)proxy-encoding-addon.node(290 lines C, wraps GStreamer)proxy-poll-addon.node(127 lines C, wraps libuvuv_poll_t)
All Linux-x64 only, built with CMake + Ninja. ABI-locked to Node major versions.
Why it's a problem:
- Linux-x64 only — no ARM64 support
- ABI breaks on Node major upgrades
- Hard to ship — must rebuild for each target platform
- Hard to debug — C N-API debugging requires core dumps
- GStreamer integration is via C, not Python
- No Python story
What you should do instead: Use pywayland (cffi bindings to
libwayland-server), python-xkbcommon, and GStreamer's Python bindings
(gi.repository.Gst). One Python process can replace the entire
compositor-proxy-cli. See 10-porting-to-odoo-js-python.md for details.
B. rc1 with no policy
What Greenfield does: All 13 packages at 1.0.0-rc1. No CHANGELOG,
no SECURITY.md, no CONTRIBUTING.md.
Why it's a problem: Breaking changes can land silently. The maintainer's roadmap is unclear.
What you should do instead: Pin to a specific commit hash if you fork it. Track upstream commits manually. Plan for the eventuality that 1.0.0 final may break APIs.
C. Encoding rigidity
What Greenfield does: Three H.264 GStreamer pipelines (x264/nvh264/ vaapih264) with hand-tuned magic constants:
bitrate=12000 vbv-buf-capacity=160 bframes=0 rc-lookahead=0- VA-API is "experimental"
- AMD (
vah264enc) absent - PNG fallback at hardcoded ≤256×256 threshold
- Browser-side hardware decode is explicitly disabled in
Session.ts:93with a FIXME "hardware decoding frame offset is wrong"
Why it's a problem:
- No adaptive bitrate — video plays at 12 Mbps regardless of network conditions
- No AMD support — limits hardware encoder options
- Browser decode is software-only — wastes GPU
What you should do instead:
- Build an adaptive multi-codec encoding router (PNG for ≤64×64 or static, VP8/H.264 for mid-size, H.264/AV1 for video)
- Drive the router by the
EncoderFeedbackmeasurement Greenfield already produces but doesn't consume - Use
pykcpor similar for congestion-aware bitrate adaptation - Enable browser hardware decode (
prefer-hardware) — fix the "frame offset is wrong" bug
D. Wire protocol has no schema version field
What Greenfield does: No version prefix on any binary message:
EncodedFrame(FRAME channel)RemoteOutOfBandChannel(PROTOCOL channel OOB)- The FD-channel in
RemoteAppLauncher.ts:612 - Undocumented magic close codes (
4200,4201,4403,4001,1005)
All break-lockstep — if you change the wire format, both sides must upgrade simultaneously or messages will be misinterpreted.
Why it's a problem:
- Cannot evolve the protocol without breaking all clients
- Cannot detect version mismatches
- Hard to debug — close codes are magic numbers
What you should do instead:
- Prefix every binary message with
u16 protocolVersion - Document every close code in a comment
- Use JSON
{"v":1, "type":"..."}for the control plane - Build a feature-flag system so old clients can negotiate capabilities
E. Main-thread decode + render
What Greenfield does:
VideoDecoderruns on main thread- WebGL context bound to
HTMLCanvasElement - No
OffscreenCanvas - No
Workerin render path - 0 uses of
OffscreenCanvasortransferControlToOffscreenin the compositor - Only 2 Web Workers used (FFmpeg Wasm fallback), shared across surfaces
Why it's a problem:
- Jank on 1080p surfaces — main thread blocks on decode
- No way to recover if main thread stalls (input also blocked)
- Can't use
requestAnimationFramefor compositing if main thread is busy
What you should do instead:
- Render in a Web Worker with
OffscreenCanvasfrom day one - Ship
EncodedVideoChunk/ArrayBuffertransfers (zero-copy) to the worker - Use
VideoFramedirectly as a WebGL texture source — nocopyTo - Use multiple workers (one per output) for parallel rendering
High severity
F. xkbcommon Wasm is multi-MB
What Greenfield does: libs/compositor-wasm/xkbcommon/build.sh
clones libxkbcommon 1.5.0, embeds the entire xkeyboard-config database
via --embed-file. Result: multi-MB WASM module loaded on every page.
Why it's a problem:
- Slow initial load
- Layout list parsed via fragile string matching
- Hard to update keyboard layouts without rebuilding WASM
What you should do instead:
- Use the browser's
navigator.keyboard.getLayoutMap()API for basic layout detection - For full xkbcommon support, lazy-load the WASM only when needed
- Consider server-side keymap compilation (Python
python-xkbcommon) and ship only the result string to the browser
G. Clipboard edge cases
What Greenfield does:
- Hardcoded MIME list
['text/plain', 'text/html', 'image/png'] - No primary selection (
wp_primary_selection_v1not implemented) - Change detection by blob size (false negatives on same-size different content)
- Focus check at
DataOffer.ts:73is a FIXME - Browser-side clipboard polling (no
clipboardchangeevent)
Why it's a problem:
- Middle-click paste doesn't work (Linux users expect it)
- Large clipboard content (e.g. copied image) is poorly handled
- MIME negotiation races
What you should do instead:
- Implement
zwp_primary_selection_device_manager_v1from day one - Use exact MIME matching, not substring
- For large clipboard content, use chunked transfer
- Watch for the
clipboardchangeevent in modern browsers (currently behind flags in Chrome)
H. XWayland/xtsb complexity
What Greenfield does:
- A full XCB reimplementation in TypeScript with its own 5800-line
xproto.xml - The XWM is a hand-port of wlroots'
xwm.cwith FIXMEs pointing at specific wlroots commits XWindowManager.ts:746literally says "this makes xterm close somehow"- Title bars painted pixel-by-pixel in
XWindowFrame.ts
Why it's a problem:
- 5800 lines of X11 protocol XML to maintain
- Hand-ported XWM logic is fragile and out of sync with upstream wlroots
- Magic workarounds in code
What you should do instead:
- Use
python-xlibon the Python proxy side instead of porting xtsb - Consider running XWayland with a minimal window manager (e.g. mutter in kiosk mode) instead of implementing XWM yourself
- For XWayland apps only (no full X11 desktop needed), the XWM can be much simpler than wlroots'
I. Adaptive bitrate feedback loop is wired but not consumed
What Greenfield does:
EncoderFeedback.tsmeasures client decode duration at 1 Hz- 4-byte feedback messages sent from browser to proxy
- Proxy receives them but does NOT use them to adapt bitrate
Channel.ts:181has a TODO confirming the gap
Why it's a problem:
- Video plays at fixed 12 Mbps regardless of network conditions
- No way to recover from congestion
- Wasted bandwidth on slow networks
What you should do instead:
- Wire
EncoderFeedbackto a bitrate adaptation controller - Use
GstElement.set_property("bitrate", ...)to adjust x264enc/nvh264enc - Implement
requestKeyUnitplumbing (Greenfield has it end-to-end but the browser never calls it)
J. Single-seat, single-session model
What Greenfield does:
- One seat per browser session
- One Node process per browser session (per
compositorSessionId) - No multi-seat support (e.g. multiple keyboards/mice for collaborative editing)
- No multi-region isolation
Why it's a problem:
- Can't support multi-user scenarios (e.g. conference room with multiple keyboards)
- No way to isolate apps between tenants
What you should do instead:
- For multi-tenant isolation: separate Python proxy processes per tenant, containerized (Docker + bubblewrap)
- For multi-seat: implement
wl_seatglobal per seat, route input events by seat
K. Custom web-bitmapbuf protocol — apps must be Greenfield-aware
What Greenfield does:
protocol/web-bitmapbuf-unstable-v1.xmlis Greenfield's custom extension- Lets browser JS apps hand a GPU-backed
ImageBitmapdirectly to the compositor - Standard Wayland apps (even browser-resident ones) fall to slow H.264 path
Why it's a problem:
- Apps must be rebuilt against
@gfld/client-protocolto use the fast path - Standard Wayland apps can't take advantage of zero-copy GPU textures
- Browser-resident WASM apps are slow unless rebuilt
What you should do instead:
- Document clearly that the fast path requires Greenfield-aware apps
- For native Linux apps (your main use case), this isn't an issue — they go through the H.264 path regardless
- For browser-resident WASM apps, encourage users to build against
@gfld/client-protocol
L. Connection loss handling
What Greenfield does:
- 10-minute zombie timeout on disconnect (
NativeAppContext.ts:152-160) - No state resumption on reconnect
eagerBufferContentscache inBufferStream.ts:31is unbounded (TODO at:87confirms the leak)- App kill timers: SIGHUP@10min, SIGKILL@+10s
- Close code 1001 → immediate SIGHUP; 4001 → immediate SIGTERM
Why it's a problem:
- User loses all running apps on browser refresh (unless they reconnect within 10 min)
- Memory leak from unbounded cache
- No way to survive proxy restart
What you should do instead:
- Bound the
eagerBufferContentscache (e.g. max 100 frames) - Persist app state on browser side (localStorage) so users can resume
- For proxy restart, checkpoint app state to disk (json file)
- Implement graceful reconnection with state sync (the KCP plumbing exists)
M. No sandboxing
What Greenfield does:
spawn(executable, args, { env })with no bubblewrap/seccomp/namespaces- Basic Auth is a single replayable credential compared with
!== compositorSessionIdis client-generated
Why it's a problem:
- Native apps have full access to the host filesystem
- One compromised app can read all other apps' data
- Basic Auth credentials can be replayed
- Client-generated session IDs can be spoofed
What you should do instead:
- Sandbox every spawned app with bubblewrap + seccomp + per-session Linux user namespace
- Replace Basic Auth with short-lived signed JWT bound to Odoo's session model
- Server-generated session IDs (use
secrets.token_urlsafe(32))
N. Heavy toolchain
What Greenfield does:
- Yarn 4 + CMake + Ninja + Meson + pkg-config
- Two different Emscripten versions: 3.1.46 for FFmpeg, 3.1.60 for SDK
- SDK builds 30+ C libraries from source
- gtk4 WASM is currently broken
- Native build needs
autoconf libtool automake meson ninja-build bison cmake build-essential gperf
Why it's a problem:
- Complex build environment setup
- Long build times
- Emscripten version skew between libs and SDK
- gtk4 WASM broken means no GTK4 browser apps
What you should do instead:
- Stick with a single Emscripten version
- Pre-build WASM modules and ship them as artifacts (don't rebuild every time)
- For Python, use
pip installfor all dependencies (simpler than CMake- Ninja)
- Skip the WASM app SDK entirely if you only care about native Linux apps
Medium / low (but worth fixing)
O. WebCodecs usage
What Greenfield does:
- WebCodecs IS used (correcting the brief in some agents' notes)
- BUT HW-decode is disabled
VideoEncoderis unused
Why it's a problem:
- Wastes GPU on software decode
- Could encode in browser (for upload to other peers) but doesn't
What you should do instead:
- Enable HW-decode by fixing the "frame offset is wrong" FIXME
- Consider WebCodecs
VideoEncoderfor any peer-to-peer video paths
P. WebCodecs hardware decode disabled
What Greenfield does:
Session.ts:93has// FIXME hardware decoding frame offset is wronghardwareAcceleration: 'prefer-software'is hardcoded
Why it's a problem:
- Software decode at 1080p60 is ~30% CPU on a modern laptop
- Battery life impact on mobile
- Wastes GPU
What you should do instead:
- Investigate and fix the frame offset bug
- Enable
prefer-hardwarefor capable browsers - Provide a runtime toggle
Q. Unbounded eagerBufferContents cache
What Greenfield does:
BufferStream.ts:31has unbounded cache- TODO at
:87confirms the leak
Why it's a problem:
- Memory leak on long-running sessions
- Can OOM the browser tab
What you should do instead:
- Bound the cache to a maximum number of frames (e.g. 100)
- Use LRU eviction
- Periodic cleanup of orphaned entries
R. H264NALDecoder.worker.ts is minimal
What Greenfield does:
- 22 lines of code
@ts-ignore'd- No error boundary
Why it's a problem:
- Worker crashes silently
- No retry on transient errors
- No metrics on decode failures
What you should do instead:
- Add error boundary with retry logic
- Log decode failures to the proxy (for diagnostics)
- Pool workers for resilience
S. Magic undocumented WebSocket close codes
What Greenfield does:
- Close codes
4200,4201,4403,4001,1005are used without documentation - Only codes 4000-4999 (application-level) tear down KCP
Why it's a problem:
- Hard to debug connection issues
- Close codes are magic numbers
What you should do instead:
- Document every close code in code comments
- Use named constants (e.g.
CLOSE_APP_TERMINATED = 4001) - Build a close-code → human-readable mapping
T. Generated protocol files committed
What Greenfield does:
packages/compositor-proxy/src/protocol/*.jsis generated but committed to the repo- Can drift from XML source
Why it's a problem:
- Easy to forget to regenerate
- PRs may include stale generated code
What you should do instead:
- Don't commit generated files
- Use
prebuildscript to generate them - Add a CI check that regenerated files match committed ones
U. Confirmed copy-paste bug in webcodec-buffer-decoder.ts
What Greenfield does:
webcodec-buffer-decoder.ts:142-144, 156-157has:this.opaqueDecoder = undefinedtwice- Should be
alphaDecoderthe second time - Leaks the alpha decoder after errors
Why it's a problem:
- Memory leak
- Wasm decoder context not cleaned up properly
- Can cause decoder pool exhaustion
What you should do instead:
- Audit all
cleanuppaths in decoder code - Add unit tests for error scenarios
Design wins to copy
Not all of Greenfield's choices are bad. Here are 12 design wins worth copying verbatim:
- Generator-driven protocol stubs (
libs/compositor-generator/,client-generator/,compositor-proxy-generator/) — single XML source for both sides. Eliminates wire-format drift. - Dual-buffer commit semantics (
Surface.ts:statevspendingState, atomic merge with pixman damage regions). Classic Wayland correctness. - UserShellApi decoupling — shell sees only
{events, actions}, never Wayland resources. This is THE integration seam you'll use. - Asynchronous browser compositor — per-client channels, never blocks on a slow client. Essential for multi-app scenarios.
- KCP-over-WebSocket for UDP-like latency over TCP (
kcp.ts, 1170 lines). Better than plain TCP for real-time video. - WebCodecs primary + WASM fallback + capability probe (
Session.ts). Modern browsers get the fast path, older ones still work. - Zero-copy dmabuf → encoder via standard
linux-dmabuf-unstable-v1(westfield-dmabuf.c). Pixels stay on GPU. - Per-surface encoder feedback channel (
EncoderFeedback.ts) measuring client decode duration — even though adaptation isn't wired up yet, the telemetry is right. ReconnectingWebSocketwith exponential backoff + message queue. Survives transient network drops.- Minimal shell — 562 lines of TypeScript total in
compositor-shell; kiosk variant is 40 lines. Demonstrates that the shell can be tiny. - Honest docs —
docs/pages/design/openly admits WebSocket/TCP is wrong for real-time and waits on WebTransport. Useful as a reference for what trade-offs were considered. - AGPL-3.0 — deliberate copyleft, but be aware before integrating. If you want to keep your modifications proprietary, fork the MIT libs and reimplement the AGPL parts.
Top-5 recommendations for the user's Odoo 20 + JS/Python compositor
-
Skip native Node addons entirely. Use
pywayland(cffi),python-xkbcommon, and GStreamer'sgi.repository.GstPython bindings. One Python process can replace the entirecompositor-proxy-cli. See10-porting-to-odoo-js-python.mdfor details. -
Sandbox every spawned app with bubblewrap + seccomp + per-session Linux user namespace; replace Basic Auth with short-lived signed JWT bound to Odoo's session model.
-
Render in a Web Worker with
OffscreenCanvasfrom day one; shipEncodedVideoChunk/ArrayBuffertransfers (zero-copy) to the worker; useVideoFramedirectly as a WebGL texture source — nocopyTo. -
Build an adaptive multi-codec encoding router (PNG for ≤64×64 or static, VP8/H.264 for mid-size, H.264/AV1 for video), driven by the
EncoderFeedbackmeasurement Greenfield already produces but doesn't consume. -
Version your wire protocol from commit #1 — prefix every binary message with
u16 protocolVersion; document every close code; use JSON{"v":1, "type":"..."}for the control plane.
License implications of AGPL-3.0
The four packages/* (compositor, compositor-proxy, compositor-proxy-cli,
compositor-shell) are AGPL-3.0-or-later. Implications:
- If you host Greenfield's
compositor-proxyas a network service and modify it, you MUST release your modifications under AGPL-3.0+. - The browser-side
@gfld/compositoris also AGPL. If you modify it (e.g. to integrate with Odoo), your modifications must be AGPL. - The lower-level
libs/*(common, client-protocol, compositor-protocol, the generators, compositor-wasm, compositor-ffmpeg-h264, xtsb) are MIT or Apache-2.0 — these are safe to use without copyleft obligations. - Strategy to avoid AGPL: use the MIT libs as-is, reimplement the
AGPL parts from scratch (browser compositor + native proxy). The
Python port described in
10-porting-to-odoo-js-python.mdis a clean way to do this — if you write your Python proxy from scratch, you can license it however you want.
What Greenfield's codebase reveals about its engineering process
The codebase reveals several engineering process insights:
- One-person show — most commits are by Erik De Rijcke. The codebase has the strengths (consistent vision, tight integration) and weaknesses (single point of failure, no code review diversity) of a one-person project.
- Long-running project — code references back to 2017-era commits
(WebRTC was a previous transport; the
kcp.tsMTU comment says "webrtc datachannel MTU"). The architecture has evolved through several major pivots. - rc1 is meaningful — the maintainer has been working toward 1.0 for years and rc1 represents a deliberate freeze. Don't expect 1.0 final any time soon, but also don't expect major architecture changes.
- Honest about limitations — docs/pages/design/ explicitly admits what's wrong and what's planned. Use this to inform your own roadmap.