Status: accepted Date: 2026-05-19
In 2.0.0, generating tabs splits into two calls: generate_arrangements(TabInput) -> Result<ArrangementSet, TabError> (exported to JS as generateArrangements), then set.render(i, width, padding, playback) -> String (the rendered ASCII tab, the [[RenderedTab]] in CONTEXT.md). ArrangementSet is a #[wasm_bindgen] struct, not a serde-serialized value, so the arrangements themselves stay Rust-side and only the rendered ASCII tabs cross the wire.
- Pattern A (serde wire). Every
Arrangement(including itsVec<Line<Vec<PitchFingering>>>) serializes to JS. Rejected: the demo never inspects fingerings, and fornum_arrangements = 20the wire payload runs ~100KB of structured data that gets thrown away. - Pattern B (opaque handle).
ArrangementSetexposes getter and render methods. Demo holds the handle across width / playback / padding changes and re-renders cheaply without re-pathfinding. Picked. - Hybrid. Opaque handle plus a separate serde-friendly view when the demo wants to introspect one arrangement. Rejected as speculative; no consumer needs it today.
- The demo on noahbaculi.com must manage the
ArrangementSetlifecycle. Callset.free()(orusingin runtimes with explicit resource management) when done;FinalizationRegistryreclaims it otherwise, but not promptly, so an explicitfree()is the recommended path. - Adding a new piece of per-arrangement information (e.g. a fingering inspector) means adding a getter method on
ArrangementSet, not a field on a serialized struct. This is the deliberate trade-off: cheaper hot path, slightly more API surface to grow. - The codebase otherwise uses
tsify-next(withserde-wasm-bindgenpulled in transitively) for boundary crossings.ArrangementSetis the documented exception;TabInput,NormalizedBeat, andTabErrorkeep the serde-via-tsify path. The rendered tab crosses as a bareString, not a serde-serialized type.