niso coupling inventory
Generated from docs/architecture/inventory.md when the site was built at 7aa23e2.
Snapshot: origin/main at 226cd3a (code identical to 3e94aa3; only DECISIONS.md moved). Paths without a crate prefix are in crates/niso-core/src/; capi: is crates/niso-capi/, glue.js is bindings/js/glue.js, rt.h is crates/niso-capi/include/niso_runtime.h. Every reference was read at that commit. Counts come from two scripts kept in ~/.capy/work/arch/tools/ (fields.py, calls2.py); they are grep-based, so the counts are approximate, but every row below was spot-checked and the false positives I found were removed.
1. The shape of the problem in one paragraph
Section titled “1. The shape of the problem in one paragraph”Dom (lib.rs:321-446) has 90 fields. 27 of them belong to a web API subsystem or to the event loop rather than to the document, and every subsystem also reaches four shared match statements and three shared loops: the ObjectData enum (lib.rs:135-196, 52 variants), interface_of (lib.rs:737-827), free_object (lib.rs:876-951), the core Task enum (lib.rs:301-312), the run loop (event_loop.rs:273-387) with its readiness predicate run_ready (event_loop.rs:175-185), the per-node cleanup in collect (lib.rs:1056-1085) and the rendering steps (lib.rs:1209-1298). The C side mirrors it: one hand-written header for every subsystem (rt.h, 1027 lines), one generator pass over all of idl/ (capi:build.rs:9-15), and one 4132-line glue.js. A new platform API therefore edits at least lib.rs, event_loop.rs, rt.h, capi:src/lib.rs and glue.js. That is what the churn shows: since 2026-10-01, of 214 non-merge commits, 50 touched glue.js, 42 lib.rs, 41 api.rs, 38 rt.h, 26 capi:src/lib.rs and 25 event_loop.rs; the in-flight branches add crypto (plat-blob-abort-crypto) and worker-host (engine-napi) rules to the same places (section 9).
2. Every field of Dom, who touches it, and where it should live
Section titled “2. Every field of Dom, who touches it, and where it should live”“Modules” is how many files read or write the field through self., d., dom. or (*dom).. “Proposed owner” is the crate that owns the state after the split in architecture.md; “core” means it stays in the document core (DOM, style, layout, render, events, input stay together, per the scoping note).
| Field | Type | Declared | Modules | Uses (file:first line(count)) | Proposed owner |
|---|---|---|---|---|---|
doc |
BaseDocument |
lib.rs:322 | 21 | api.rs:93(91), lib.rs:584(39), tree.rs:48(24), input.rs:81(20), loading.rs:213(15), animation_style.rs:52(15), events.rs:193(14), api/cssom.rs:68(14), api/documents.rs:58(7), api/custom_elements.rs:124(7), api/attributes.rs:34(6), template.rs:169(5), geometry.rs:20(4), render.rs:439(3), event_loop.rs:131(3), serialize.rs:51(2), api/traversal.rs:123(2), api/history.rs:35(2), api/namespaces.rs:34(2), api/resize.rs:77(2), api/base.rs:15(1) | core |
objects |
SlotMap<ObjKey, Object> |
lib.rs:323 | 2 | lib.rs:579(10), worker.rs:167(1) | niso-host (table type); core keeps the instance |
node_refs |
HashMap<NodeId, u32> |
lib.rs:324 | 1 | lib.rs:842(7) | core |
listeners |
HashMap<Obj, Vec<Rc<Listener>>> |
lib.rs:325 | 2 | events.rs:212(5), lib.rs:878(2) | core (events) |
pending_moves |
Vec<(Obj, Obj)> |
lib.rs:326 | 1 | lib.rs:993(4) | core |
candidates |
Vec<NodeId> |
lib.rs:327 | 1 | lib.rs:858(6) | core |
tree_hook |
Option<(TreeHookFn, *mut c_void)> |
lib.rs:328 | 1 | lib.rs:666(3) | core |
inline_styles |
HashMap<NodeId, Obj> |
lib.rs:329 | 2 | api.rs:2577(2), lib.rs:881(1) | core |
token_lists |
HashMap<NodeId, Obj> |
lib.rs:330 | 2 | api.rs:1864(2), lib.rs:885(1) | core |
string_maps |
HashMap<NodeId, Obj> |
lib.rs:331 | 2 | api.rs:2921(2), lib.rs:889(1) | core |
contexts |
HashMap<NodeId, Obj> |
lib.rs:333 | 2 | api.rs:3821(2), lib.rs:922(1) | niso-canvas |
canvases |
HashMap<NodeId, canvas::Bitmap> |
lib.rs:334 | 4 | canvas.rs:91(6), lib.rs:1060(4), render.rs:143(2), api.rs:3825(1) | niso-canvas |
web_sockets |
websocket::WebSockets |
lib.rs:335 | 1 | websocket.rs:273(13) | niso-websocket |
pointers |
input::Pointers |
lib.rs:336 | 1 | input.rs:154(18) | core (input) |
media_queries |
Vec<Obj> |
lib.rs:338 | 3 | lib.rs:925(2), event_loop.rs:184(1), api.rs:2494(1) | core (style) |
template_of |
HashMap<NodeId, NodeId> |
lib.rs:340 | 2 | lib.rs:1034(3), tree.rs:425(1) | core |
input_values |
HashMap<NodeId, String> |
lib.rs:341 | 4 | events.rs:527(5), api.rs:3230(5), lib.rs:1061(4), tree.rs:394(2) | core (forms) |
input_checked |
HashMap<NodeId, bool> |
lib.rs:342 | 4 | events.rs:385(4), lib.rs:1062(4), api.rs:3497(3), tree.rs:397(2) | core (forms) |
selection |
api::forms::Selection |
lib.rs:343 | 1 | api/forms.rs:91(7) | core (forms) |
control_sizes |
[[f32; 2]; 5] |
lib.rs:345 | 1 | lib.rs:1336(2) | core (controls) |
control_font_size |
f32 |
lib.rs:346 | 1 | lib.rs:1353(3) | core (controls) |
button_style |
controls::ButtonStyle |
lib.rs:347 | 1 | lib.rs:1367(3) | core (controls) |
field_style |
controls::FieldStyle |
lib.rs:348 | 2 | lib.rs:1308(6), events.rs:527(3) | core (controls) |
controls_sheet |
UserAgentStylesheet |
lib.rs:349 | 1 | lib.rs:1390(2) | core (controls) |
changed_controls |
HashSet<NodeId> |
lib.rs:351 | 2 | events.rs:625(5), lib.rs:1063(1) | core (controls) |
window |
Obj |
lib.rs:352 | 8 | api.rs:1180(4), events.rs:189(3), lib.rs:578(3), worker.rs:166(3), input.rs:209(3), api/history.rs:57(2), messaging.rs:705(1), api/messaging.rs:26(1) | core |
started |
Instant |
lib.rs:353 | 2 | lib.rs:676(1), messaging.rs:657(1) | niso-sched (clock) |
clock_offset |
f64 |
lib.rs:355 | 2 | lib.rs:676(2), messaging.rs:657(1) | niso-sched (clock) |
scratch_str |
String |
lib.rs:356 | 1 | ffi.rs:295(2) | core (ffi) |
scratch_handles |
Vec<u64> |
lib.rs:357 | 1 | ffi.rs:307(2) | core (ffi) |
scratch_strings |
Vec<String> |
lib.rs:358 | 1 | ffi.rs:312(1) | core (ffi) |
scratch_strs |
Vec<NisoStr> |
lib.rs:359 | 1 | ffi.rs:313(2) | core (ffi) |
scratch_bytes |
Rc<[u8]> |
lib.rs:360 | 1 | ffi.rs:323(2) | core (ffi) |
scratch_text |
Rc<str> |
lib.rs:361 | 1 | ffi.rs:350(2) | core (ffi) |
last_error |
String |
lib.rs:362 | 1 | ffi.rs:187(2) | core (ffi) |
render |
render::RenderState |
lib.rs:363 | 1 | lib.rs:1167(5) | core (render) |
surfaces |
template::SurfaceState |
lib.rs:364 | 1 | lib.rs:1318(4) | niso-surfaces |
net |
std::sync::Arc<net::Net> |
lib.rs:365 | 5 | event_loop.rs:178(5), lib.rs:594(4), websocket.rs:452(4), loading.rs:196(3), messaging.rs:664(1) | niso-fetch (transport); core keeps a ResourceLoader handle |
net_scratch |
Vec<u8> |
lib.rs:366 | 1 | lib.rs:643(3) | niso-fetch |
broken_images |
HashSet<String> |
lib.rs:368 | 2 | lib.rs:1269(1), api.rs:3168(1) | core (loading) |
detached_images |
HashMap<NodeId, String> |
lib.rs:371 | 2 | lib.rs:1058(2), tree.rs:240(1) | core (loading) |
media |
media::MediaState |
lib.rs:372 | 4 | api.rs:158(12), lib.rs:606(9), render.rs:179(5), event_loop.rs:183(1) | niso-media |
idb |
indexeddb::Idb |
lib.rs:373 | 3 | indexeddb/mod.rs:466(43), api/indexeddb.rs:383(4), api.rs:2347(1) | niso-indexeddb |
media_scratch |
Vec<u8> |
lib.rs:374 | 1 | lib.rs:630(5) | niso-media |
pending_scrolls |
Vec<NodeId> |
lib.rs:377 | 2 | lib.rs:1155(3), event_loop.rs:182(1) | core (rendering steps) |
media_queries_stale |
bool |
lib.rs:379 | 2 | lib.rs:658(2), event_loop.rs:184(1) | core (style) |
canvas_src |
std::cell::Cell<Option<bool>> |
lib.rs:382 | 3 | loading.rs:229(2), event_loop.rs:136(2), api/mutation.rs:89(1) | core (animating check) |
event_loop |
event_loop::EventLoop |
lib.rs:383 | 1 | event_loop.rs:62(24) | niso-sched |
modules |
modules::Modules |
lib.rs:384 | 2 | modules.rs:228(15), websocket.rs:266(2) | niso-modules |
scripts |
loading::Scripts |
lib.rs:385 | 2 | loading.rs:200(18), api.rs:3400(2) | core (loading) |
session |
api::history::Session |
lib.rs:386 | 4 | api/history.rs:31(14), media.rs:204(4), loading.rs:214(1), storage/mod.rs:382(1) | core (history) |
location |
Option<Obj> |
lib.rs:387 | 3 | api.rs:2410(2), api/messaging.rs:283(1), api/location.rs:10(1) | core (history) |
history |
Option<Obj> |
lib.rs:388 | 2 | api.rs:2415(1), api/history.rs:67(1) | core (history) |
node_iterators |
Vec<Obj> |
lib.rs:390 | 1 | api/traversal.rs:54(3) | core |
documents |
HashMap<NodeId, api::documents::DocumentData> |
lib.rs:392 | 2 | api/documents.rs:62(3), lib.rs:816(2) | core |
node_documents |
HashMap<NodeId, NodeId> |
lib.rs:394 | 2 | api/documents.rs:58(4), lib.rs:1069(1) | core |
node_kinds |
HashMap<NodeId, api::documents::NodeKind> |
lib.rs:397 | 5 | api/attributes.rs:17(4), lib.rs:803(3), api/documents.rs:101(3), tree.rs:391(2), api.rs:406(1) | core |
implementations |
HashMap<NodeId, Obj> |
lib.rs:399 | 2 | api/documents.rs:142(2), lib.rs:901(1) | core |
attr_nodes |
HashMap<NodeId, Vec<NodeId>> |
lib.rs:401 | 2 | api/attributes.rs:40(5), api/documents.rs:79(1) | core |
attribute_maps |
HashMap<NodeId, Obj> |
lib.rs:403 | 2 | api/attributes.rs:185(2), lib.rs:905(1) | core |
scripting |
std::sync::Arc<std::sync::atomic::AtomicBool> |
lib.rs:405 | 1 | loading.rs:108(4) | core (loading) |
resize_observers |
Vec<Obj> |
lib.rs:407 | 1 | api/resize.rs:97(5) | core (style) |
style_sheets |
HashMap<NodeId, Obj> |
lib.rs:408 | 2 | api/cssom.rs:78(2), lib.rs:894(1) | core (cssom) |
style_sheet_list |
Option<Obj> |
lib.rs:409 | 1 | api.rs:1186(2) | core (cssom) |
cssom_objects |
Vec<Obj> |
lib.rs:411 | 1 | api/cssom.rs:55(5) | core (cssom) |
registered |
HashMap<NodeId, Vec<api::mutation::Registered>> |
lib.rs:413 | 1 | api/mutation.rs:72(11) | core (mutation) |
mutation_observers |
Vec<Obj> |
lib.rs:415 | 1 | api/mutation.rs:267(3) | core (mutation) |
pending_mutation_observers |
Vec<Obj> |
lib.rs:416 | 1 | api/mutation.rs:158(6) | core (mutation) |
mutation_microtask_queued |
bool |
lib.rs:417 | 1 | api/mutation.rs:206(3) | core (mutation) |
mutation_microtask_signal |
bool |
lib.rs:420 | 3 | api/mutation.rs:208(2), animation.rs:863(1), api/custom_elements.rs:199(1) | core (mutation; also written by animation.rs) |
microtask_hook |
Option<(api::mutation::MicrotaskHookFn, *mut c_void)> |
lib.rs:421 | 1 | api/mutation.rs:213(2) | core (mutation) -> niso-sched checkpoint hook |
anim |
animation::Animations |
lib.rs:422 | 4 | animation.rs:392(29), animation_style.rs:427(8), event_loop.rs:315(1), api/animations.rs:169(1) | niso-animations |
local_storage |
Option<Obj> |
lib.rs:423 | 1 | api.rs:2425(1) | niso-webstorage |
screen |
Option<Obj> |
lib.rs:424 | 1 | api.rs:2438(1) | core |
custom_element_registry |
Option<Obj> |
lib.rs:425 | 1 | api.rs:2443(1) | core |
custom_elements |
api::custom_elements::CustomElements |
lib.rs:426 | 1 | api/custom_elements.rs:121(31) | core |
crypto |
Option<Obj> |
lib.rs:427 | 1 | api.rs:2342(1) | niso-crypto |
performance |
Option<Obj> |
lib.rs:428 | 2 | api/timing.rs:119(9), api.rs:2337(1) | niso-performance |
performance_entries |
Vec<Obj> |
lib.rs:430 | 1 | api/timing.rs:82(6) | niso-performance |
time_origin |
f64 |
lib.rs:432 | 1 | api/timing.rs:125(1) | niso-performance |
session_storage |
Option<Obj> |
lib.rs:433 | 1 | api.rs:2433(1) | niso-webstorage |
storage |
storage::Storage |
lib.rs:434 | 4 | api/storage.rs:59(9), worker.rs:160(2), storage/mod.rs:336(2), indexeddb/mod.rs:461(1) | niso-storage (drivers) + niso-webstorage (areas) |
cookies |
Vec<(String, String)> |
lib.rs:436 | 1 | api.rs:1264(3) | core (document.cookie) |
tasks |
std::collections::VecDeque<Task> |
lib.rs:437 | 3 | lib.rs:1396(2), websocket.rs:169(1), event_loop.rs:176(1) | niso-sched (core keeps its DOM-manipulation queue as a Source) |
load_event |
Option<VoidFunction> |
lib.rs:439 | 3 | websocket.rs:452(2), lib.rs:1283(1), api.rs:2404(1) | core (loading) |
messaging |
messaging::Messaging |
lib.rs:440 | 6 | messaging.rs:444(17), worker.rs:168(14), websocket.rs:452(4), event_loop.rs:181(2), api/messaging.rs:151(2), modules.rs:259(1) | niso-messaging |
worker |
Option<Box<worker::Scope>> |
lib.rs:442 | 3 | worker.rs:175(13), api/messaging.rs:283(2), messaging.rs:525(1) | niso-messaging (workers) |
worker_runner |
Option<WorkerRunnerFn> |
lib.rs:443 | 1 | worker.rs:170(3) | niso-messaging (workers) |
message_hook |
Option<(MessageHookFn, *mut c_void)> |
lib.rs:444 | 2 | lib.rs:662(1), messaging.rs:644(1) | niso-messaging |
wasm |
WasmSlot |
lib.rs:445 | 1 | lib.rs:1408(3) | niso-wasm |
Summary of the table:
- 62 fields stay in the core. Most are touched by 1 or 2 files, which is healthy:
doc(21 files) is the document itself. - 27 fields move to a web API crate or to the scheduler, and
objectskeeps its place but becomes niso-host’s table type. The ones touched outside their own subsystem are the coupling:net(event_loop.rs, websocket.rs, loading.rs, messaging.rs),messaging(worker.rs, websocket.rs, event_loop.rs, api/messaging.rs, modules.rs),media(api.rs, render.rs, event_loop.rs),anim(animation_style.rs, event_loop.rs, api/animations.rs),storage(worker.rs, indexeddb/mod.rs),canvases(render.rs, api.rs),tasks(event_loop.rs, websocket.rs),load_event(websocket.rs:452, lib.rs:1283, api.rs:2404),modules(websocket.rs:266). - Three fields are the clock and the loop (
started,clock_offset,event_loop) and become niso-sched. mutation_microtask_signalis written by animation.rs:863 and api/custom_elements.rs:199 as well as its owner, which is how animations and custom elements piggyback on the mutation-observer microtask. Under the design this becomes a checkpoint hook.
3. Central match statements and lists that every subsystem edits
Section titled “3. Central match statements and lists that every subsystem edits”| Place | What each subsystem adds | Evidence |
|---|---|---|
ObjectData |
one or more variants per object type | lib.rs:135-196 (IdbRequest…IdbCursor 177-182, MessagePort..WorkerLocation 184-191, Animation/KeyframeEffect/Timeline 192-195, WebSocket 173, Canvas2D 172) |
interface_of |
one arm per variant | lib.rs:737-797 |
free_object |
cleanup arm that reaches into the subsystem’s state | lib.rs:876-951 (free_idb 927, free_animation 929, media_queries.retain 925, message ports 945) |
Task (core queue) |
a variant per task source the core queues for | lib.rs:301-312 (Idb, Wasm), run in lib.rs:1426-1449 |
run_ready |
an || clause |
event_loop.rs:175-185 (modules 177, net 178-179, WebSockets 180, messaging 181, media 183) |
Dom::run |
a branch | event_loop.rs:283-312 (WebSocket and modules 285, messages 289, fetch task end 299-308) |
wait_network |
knowledge of every source that can deliver from another thread | websocket.rs:446-513 reads messaging (452, 466, 485), net (452, 466, 474), workers (447, 484) and the load event (452) |
collect |
per-node side-table cleanup | lib.rs:1057-1079: images, listeners, canvases, input values, scripts, mutation registrations, custom elements, media, documents, attrs |
| rendering steps | a step | lib.rs:1209-1298: scrolls, media events (1217), media queries, image loads via net (1248), load event (1282), animations (1290), animation frames (1291), resize observers (1293) |
| C call epilogue | a delivery after every C API call | ffi.rs:194-208 and 221-235: tree moves, scripts, element queue, mutation microtask, animation promises (deliver_animation_promises, animation.rs:522) |
| checkpoint end | cleanup | event_loop.rs:216-220 calls idb_cleanup (indexeddb/mod.rs:812) |
EventExtra |
a payload variant per event interface | events.rs:52-73 (Message, Close, Storage, VersionChange, AnimationPlayback, MediaQuery) |
| event path | non-node parents | events.rs:294-312 hard-codes idb_parent (indexeddb/mod.rs:732) for non-node targets |
ErrorCode |
codes | lib.rs:82-119; plat-blob-abort-crypto adds Operation = 207 |
| Window getters | a singleton field plus getter per API | api.rs:2334-2349 (performance, crypto, indexedDB), 2419-2445 (localStorage, sessionStorage, screen, customElements) |
html_interface |
element interfaces | lib.rs:199-277 (stays in core; elements are the document’s) |
4. Call graph between subsystems
Section titled “4. Call graph between subsystems”Edges are “file A calls a function defined only in file B” with count and one example. The full list (183 edges, std-named functions filtered out) is tools/calls2.txt; this is the subsystem-relevant part.
4.1 Into the run loop (who the loop calls)
Section titled “4.1 Into the run loop (who the loop calls)”| From | To | Through | Ref |
|---|---|---|---|
| event_loop | modules | module_tasks_pending, module_budget, run_module_task |
event_loop.rs:177, 275, 285 |
| event_loop | net | has_done, has_loads, done_ids, drop_done |
event_loop.rs:178-179, 299-307 |
| event_loop | websocket | web_sockets_pending, web_sockets_unwatched, run_task, wait_network |
event_loop.rs:180, 197, 285, 338 |
| event_loop | messaging | has_items, message_limit, run_message, answer_loads, waits_past |
event_loop.rs:181, 281-282, 289, 297 |
| event_loop | worker | worker_closing |
event_loop.rs:278 |
| event_loop | animation | animations_need_frames, anim.last_update |
event_loop.rs:131, 315 |
| event_loop | indexeddb | idb_cleanup |
event_loop.rs:219 |
| event_loop | lib (render steps) | rendering_steps, update_rendering, run_tasks |
event_loop.rs:283, 309, 382 |
| lib (rendering_steps) | media, net, animation, resize | media.events, net.take_loads, update_animations, deliver_resize_observations |
lib.rs:1217, 1248, 1290, 1293 |
| lib (run_tasks) | indexeddb, wasm, history | indexeddb::run_task, wasm().run_task, history::traverse |
lib.rs:1437-1444 |
4.2 Between web API subsystems
Section titled “4.2 Between web API subsystems”| From | To | Count | Through | Ref |
|---|---|---|---|---|
| websocket | messaging | 6 | children_active, has_items, agent.wait, Message, message_event |
websocket.rs:452, 466, 485, 550-551 |
| websocket | net | 5 | resources_pending, has_done, busy, has_loads |
websocket.rs:452, 466, 474 |
| websocket | modules | 2 | the shared Waker |
websocket.rs:26, 266 |
| websocket | worker / event_loop | 3 | is_worker, nothing_scheduled |
websocket.rs:447, 484 |
| messaging | worker | 3+ | worker_failed, worker::Shared |
messaging.rs:27-ish imports, 715 |
| worker | messaging | 10 | adopt, agents, ports |
worker.rs:168-178 |
| worker | storage | 3 | configure for the worker’s DOM |
worker.rs:160 |
| worker | loading | 3 | load_resource (the window’s loader) |
worker.rs:133 |
| messaging | net | 1 | load_local (answers a worker’s load) |
messaging.rs:664 |
| net | loading, modules | 2 | Loader, Waker |
net.rs:14-15 |
| api/messaging | worker | 19 | start_worker and the Worker members |
api/messaging.rs:253 |
| indexeddb | storage | 3 | driver_for, storage_origin |
indexeddb/mod.rs:461, 501 |
| indexeddb, key_path, storage/key | structured_clone | 32 | js_string, own_property, buffer_source |
indexeddb/key_path.rs:52, storage/key.rs:187 |
| api/storage | storage | 3 | driver_for |
api/storage.rs:67 |
| animation_style | animation | 8 | effect, keyframes |
animation_style.rs:204 |
| animation | animation_style | 1 | target_properties |
animation.rs:1160 |
| animation | api/mutation | 1 | deliver_microtask |
animation.rs:1243 |
| api/mutation | animation | 1 | notify_finished_animations |
api/mutation.rs:288 |
| render | media, controls, template | 7 | shown_state, control, is_surface |
render.rs:401, 504, 489 |
| template | render, controls | 4 | radii, control |
template.rs:209, 190 |
Two things stand out. Nothing in storage/, structured_clone.rs, indexeddb/key_path.rs, net.rs (except two imports), wasm.rs or media.rs reaches into Dom (section 7), so they are close to standalone already. The genuinely tangled cluster is websocket + messaging + worker + net, and the tangle is entirely about the event loop: “can anything still arrive from another thread, and how do I wait for it” (wait_network, websocket.rs:446-513). That knowledge belongs to the scheduler, fed by each source.
4.3 Into events (who fires events, and how)
Section titled “4.3 Into events (who fires events, and how)”Every subsystem fires through Dom::fire_at / Dom::fire / Dom::dispatch (events.rs:343, 495, 503) with a raw *mut Dom, which is how script reenters: websocket.rs:538-564, messaging.rs:631-651 (dispatch_made), worker.rs:298, indexeddb/mod.rs:1214-1217, animation.rs:1259, input.rs:229, lib.rs:1214-1279 (rendering steps). Each needs a payload variant in EventExtra (events.rs:52-73) and IndexedDB needs the event path special case (events.rs:304-309).
5. Where the C API, api.rs, the IDL and glue.js tie subsystems together
Section titled “5. Where the C API, api.rs, the IDL and glue.js tie subsystems together”5.1 IDL and generated code
Section titled “5.1 IDL and generated code”- One pass over all of
idl/(17 files, 2671 lines) generates: core’sInterfaceIdenum and callback types (crates/niso-core/build.rs:2-6, included at lib.rs:69-72), capi’s C shims andniso.h(capi:build.rs:9-15), QuickJS’s host table and the generated part of the glue (crates/niso-quickjs/build.rs:17-20, spliced at glue.js:1098/*@GENERATED@*/), Porffor’s (crates/niso-porffor/build.rs:56-57) and the JVM classes. - Every generated shim calls
niso_core::api::<snake interface>::<member>(crates/niso-idl/src/emit.rs:504, 524, 544, 559, 580, 593). So the implementation of every interface, whichever subsystem owns it, has to be reachable atapi::<iface>in niso-core: api.rs:3699mod indexeddb, 3705web_socket, 3750mod messaging, 3786html_canvas_element, 3835canvas_rendering_context2_d, 2962html_media_element, 2507crypto, 2300-2350window_or_worker_global_scope(timers, performance, crypto, indexedDB). - The IDL is already split by subsystem file, and three subsystems already add themselves to the global with partials: indexeddb.webidl:75, webcrypto.webidl:6, performance.webidl:9. Web Storage (
partial interface Window, html.webidl:21-26, Storage 33, StorageEvent 55) and HTMLMediaElement (html.webidl:361) are still in html.webidl. - Extended attributes in use: 64
[NisoGlue]and 64[NisoInternal]members (implemented or called only by the glue), 17[NisoReentrant], 10[NisoHeld], 2[NisoNative], 2[NisoValue].[NisoGlue]members tie an IDL file to a specific section of glue.js.
5.2 The hand-written C API (capi:src/lib.rs, 682 lines; rt.h, 1027 lines)
Section titled “5.2 The hand-written C API (capi:src/lib.rs, 682 lines; rt.h, 1027 lines)”| Subsystem | capi:src | rt.h |
|---|---|---|
| DOM lifecycle, hooks, collect | lib.rs:18-205 | 274-323 |
| Storage | lib.rs:155-170 | 112-272 (driver vtable), 312-313 |
| Event loop | lib.rs:71, 101-115 | 324-379 |
| Workers and messages | lib.rs:76-100 | 381-401 |
| Render stream, surfaces, templates | lib.rs:208-240, 529-565 | 402-664, 783-789 |
| Input and controls | lib.rs:242-345, 462-527 | 665-782 |
| Net (fetch transport) | lib.rs:351-395 | 791-810 |
| Media and media session | lib.rs:396-460 | 813-836 |
| Native modules | lib.rs:586-690 | 837-885 |
| WebAssembly | capi:src/wasm.rs (263) | 886-959 |
| Structured clone decode | capi:src/clone.rs (305) | 961-1018 |
| Handles | lib.rs:567-584 | 1020-1027 |
Bindings install nine hooks to make the subsystems work: task (rt.h:284), tree (283), script (285), microtask (289), message (394), worker runner (393), module (885), wasm call and task (920) and wake (379). The wake hook is one Waker shared by modules, the network and the window’s messaging agent (lib.rs:470-471, modules.rs:257-260, messaging.rs:94-95).
5.3 glue.js (4132 lines) by section
Section titled “5.3 glue.js (4132 lines) by section”| Lines | Section | Owner after the split |
|---|---|---|
| 1-235 | wrapper identity, cache, FinalizationRegistry, indexed/named proxies | core glue |
| 239-880 | SC, structured serialization |
niso-structured-clone glue |
| 881-1100 | WebIDL conversions, callbacks table (cbs, 971), [NisoGlue] plumbing, /*@GENERATED@*/ |
core glue |
| 1100-1185 | DOMException, QuotaExceededError, makeError |
core glue (niso-base) |
| 1186-1712 | IndexedDB [NisoGlue] members |
niso-indexeddb glue |
| 1713-1880 | global, postMessage, MessageEvent data, workers | niso-messaging glue |
| 1881-2095 | CSSOM camel-case, custom elements | core glue |
| 2096-2163 | console, reportError | core glue |
| 2164-2195 | timers | niso-sched glue |
| 2196-2338 | fetch, Headers, Response over niso_net_* |
niso-fetch glue |
| 2339-2466 | media play promises, media session | niso-media glue |
| 2467-2498 | globals list (fetch, navigator, …) |
composed |
| 2499-2869 | Web Animations | niso-animations glue |
| 2870-2994 | script processing | core glue |
| 2995-3001 | tasks(): the glue’s own task sources, fetch and media actions |
removed: become Rust sources |
| 3002-3231 | native modules | niso-modules glue |
| 3232-4110 | WebAssembly JS API | niso-wasm glue |
| 4110-4132 | the hooks object returned to the engine | composed |
The glue runs two task sources of its own (settleFetches and runMediaActions, glue.js:2998-3000) from the engine’s task hook (QE_HOOK_TASKS, niso_js.c:931-937). That is why the Rust run loop has a special branch that gives finished fetches a task end and then drops what nobody took (event_loop.rs:299-308): the scheduler cannot see a source that lives in JS.
6. Shared global state
Section titled “6. Shared global state”| State | Kind | Ref | Notes |
|---|---|---|---|
PORTS |
LazyLock<Mutex<HashMap<u64, Port>>> |
messaging.rs:217 | process-wide port registry: entangled ports, owner agent, queue |
NEXT_ID |
AtomicU64 |
messaging.rs:218 | port ids |
CHANNELS |
LazyLock<Mutex<Vec<Channel>>> |
messaging.rs:381 | BroadcastChannel registry |
POSTS |
AtomicU64 |
messaging.rs:100 | race detection in children_active |
REMOVING |
thread_local! |
messaging.rs:43-48 | worklist for port removal |
SURFACES_SHEET, DEFAULT_CONTROLS |
LazyLock<UserAgentStylesheet> |
lib.rs:461-465 | immutable, fine |
current |
_Thread_local niso_js* |
bindings/js/niso_js.c:41 | the engine per thread |
Waker |
Arc, shared by modules, net, agent |
lib.rs:470-471 | one wake hook for every cross-thread producer |
| worker threads | one full Dom each |
worker.rs:157-158 | Dom::new(1, 1, 1.0) builds a blitz document and stylist for a worker that never renders |
| storage directory | env and platform dirs | storage/mod.rs (default_directory), docs/storage.md |
resolved per DOM |
No other statics exist in the workspace crates (grep for static, thread_local!, LazyLock, OnceLock across crates/*/src). The messaging registries are legitimately process-wide (ports move between threads), so the design keeps them, inside niso-messaging, with their lifetime documented in its contract.
7. How close each subsystem already is to standalone
Section titled “7. How close each subsystem already is to standalone”Measured as references to Dom internals in the file:
| File | Lines | impl Dom |
*mut Dom fns |
ObjectData:: |
Imports from the crate |
|---|---|---|---|---|---|
| structured_clone.rs | 804 | 0 | 0 | 0 | ErrorCode, Result, err only (structured_clone.rs:18) |
| storage/{ffi,key,memory,sqlite}.rs | 1339 | 0 | 0 | 0 | ErrorCode::QuotaExceeded, structured_clone::js_string |
| storage/mod.rs | 494 | 1 | 0 | 0 | Dom, api::storage::Area (the impl Dom is the setup API) |
| indexeddb/key_path.rs | 267 | 0 | 0 | 0 | storage::key::Key, structured_clone |
| wasm.rs | 950 | 0 | 0 | 0 | Error, Dom::queue_wasm_task |
| net.rs | 315 | 0 | 0 | 0 | loading::Loader, modules::Waker |
| media.rs | 235 | 0 | 0 | 0 | none |
| modules.rs | 339 | 1 | 2 | 0 | crate root |
| websocket.rs | 589 | 1 | 1 | 5 | events, ffi, messaging::Message, Waker |
| messaging.rs + api/messaging.rs | 1251 | 1 | 6 | 11 | events, ffi, worker::Shared, Waker |
| worker.rs | 455 | 1 | 4 | 5 | messaging, storage, loading, events |
| indexeddb/mod.rs + ops.rs + api/indexeddb.rs | 2663 | 5 | 5 | 19 | storage, structured_clone, events |
| animation.rs + api/animations.rs | 1743 | 1 | 2 | 10 | animation_style, events, mutation |
| animation_style.rs | 541 | 1 | 0 | 0 | 15 uses of self.doc (stylo) |
| template.rs | 398 | 0 | 0 | 0 | 5 uses of doc |
| canvas.rs | 172 | 1 | 0 | 0 | crate root |
8. Hot paths
Section titled “8. Hot paths”8.1 Style and layout flush
Section titled “8.1 Style and layout flush”flush_layout (lib.rs:1114-1127) runs on every frame, surface render, template render, scroll (lib.rs:1146), hit test (input.rs:94) and geometry read. It calls three subsystems in a fixed order: doc.handle_messages, disable_links (loading.rs:403), apply_animated_styles (animation_style.rs:468), then doc.resolve under catch_unwind with a retry. render_frame then remakes every canvas bitmap (lib.rs:1305-1307) and walks the tree with a fresh nodes: HashMap::new() each frame (render.rs:208-217).
8.2 Event dispatch
Section titled “8.2 Event dispatch”Dom::dispatch (events.rs:343-392) and dispatch_to_path (394-452): per dispatch it allocates the path (event_path, events.rs:294) and clones it into the event (e.path = path.clone(), 409); invoke (454-492) runs once per path entry per phase and each time clones the event type String, looks the target up in the listeners HashMap and collects matching listeners into a new Vec<Rc<Listener>>. A click on an element 20 deep is about 40 hash lookups and 40 vector allocations before any listener runs. Every event is a slotmap object (create_event, events.rs:181) freed afterwards.
8.3 The run loop and frame scheduling
Section titled “8.3 The run loop and frame scheduling”needs_frame(event_loop.rs:163-165), which a presenting host calls every display frame, evaluatesrun_ready: about four mutex locks plus one per native module (net.rs:143-150, messaging.rs:139-141, modules.rs:309-311), andanimating(130-132), which scans the whole tree (css_animating, 149-151) whenever blitz reports animating and the document has a<canvas src>.run(273-387) per iteration:run_tasksswaps out the wholeVecDeque(lib.rs:1427, so its buffer is dropped and regrown),module_budgetallocates aVecper run (modules.rs:303-306), timers are found by a linearmin_by(316-318), andcollectruns after every task (287, 296, 310, 385).- The JS binding’s task hook (niso_js.c:931-937) runs a checkpoint, the glue’s
tasks()hook, andqj_sweep(niso_js.c:149-153), which scans the whole id table, so a page holding 100k wrappers pays 100k iterations at the end of every task.
8.4 C API calls (every DOM call from every language)
Section titled “8.4 C API calls (every DOM call from every language)”ffi::call and call_reentrant (ffi.rs:194-235) wrap each call in push_element_queue, catch_unwind, and five deliveries: tree moves, scripts, the element queue, the mutation microtask and animation promises. Each is cheap when empty, but the list is a coupling point: animations hook every C call.
8.5 Mutation output
Section titled “8.5 Mutation output”The render stream (render.rs, RenderState 87-98, frame 208) and the templates (template.rs, SurfaceState) consume doc, form state (controls::Forms), media state and canvases, borrowed together in lib.rs:1308-1309 and 1317-1318. That is the only place the renderer reads subsystem state, so a RenderContributor hook (media, canvas) is enough to decouple it.
9. In-flight branches against these hotspots
Section titled “9. In-flight branches against these hotspots”Diffs are git diff origin/main...origin/<branch>.
| Branch | Base | Commits | Hotspots touched |
|---|---|---|---|
| storage-fixes (merging next) | 52608766 | 13 | storage/* (mod.rs +290, sqlite.rs +203, ffi.rs, key.rs, memory.rs), structured_clone.rs (+197), event_loop.rs (+39, restructures run), lib.rs (+32), rt.h, capi lib.rs, glue.js, worker.rs, api/storage.rs |
| sched-followups | f500e555 | 5 | event_loop.rs (+19), modules.rs (+34), websocket.rs (+164) |
| plat-blob-abort-crypto | 26e16680 (63 commits behind main) | 42 | glue.js (+2144), api.rs (+74), new api/abort.rs, api/crypto.rs, event_loop.rs (+27: crypto_busy in run_ready, a crypto branch in run), events.rs, lib.rs (+35: AbortController/AbortSignal variants, crypto_jobs, secure_context, abort_algorithms fields), messaging.rs, net.rs (+144), structured_clone.rs, websocket.rs, worker.rs, idl (dom, url, webcrypto, workers, xhr), new crate niso-crypto |
| engine-napi | 52608766 | 31 | glue.js (+155), niso_js.c, rt.h (+31: worker host), capi lib.rs, lib.rs (+35: object_owner), messaging.rs, worker.rs (+141), new crate niso-napi |
| tv-input | not on origin | ? | from its commit message (~/.capy/work/tv-input/commitmsg.txt): Gamepad API, controller keys, focus ring, niso_dom_gamepad_*, niso_dom_set_input, niso_dom_focus_ring, niso_tv_viewport, so input.rs, events.rs, lib.rs, rt.h, capi, glue.js, a new IDL file. Not verified. |
| native-look | local only | ? | not inspected; by name controls.rs, render.rs and hosts. Not verified. |
The storage-fixes run loop was written against a base 16 event_loop.rs commits behind main (git log 52608766..origin/main -- event_loop.rs), and plat-blob-abort-crypto adds a third set of rules (crypto_busy) to the same predicate. Every one of these is a subsystem teaching the shared loop about itself.