@rowsncolumns/rnc-yrs-store
v0.9.14
Published
Document-backed Rust→WASM spreadsheet calc engine (yrs/CRDT, canonical sheetDataV3). Ships the `SpreadsheetEngine` (a `Store<YrsStore>`): applyCommand / undo / redo / canUndo + the Yjs collaboration surface (applyUpdate / takePendingUpdates / loadFromUpda
Readme
vendored rnc-yrs-store
Self-built artifact of the document-backed Rust→WASM spreadsheet calc engine from
rowsncolumns/spreadsheet#168.
This is the rnc-yrs-store crate's SpreadsheetEngine: a Store<YrsStore> whose authoritative
state lives in a yrs (yjs-compatible) CRDT document, written in the canonical sheetDataV3
encoding. It supersedes the earlier in-memory rnc-wasm preview build — it exposes the same
addSheet / applyCommand / undo / redo / canUndo / canRedo surface plus the
collaboration surface (applyUpdate / takePendingUpdates / loadFromUpdate / stateAsUpdate)
that the Keryx update-pump binds to.
The package (@rowsncolumns/rnc-engine) is not yet published to npm, so the engine is vendored here.
Contents
| File | Role |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- |
| rnc_yrs_store.js | wasm-bindgen JS glue (the SpreadsheetEngine class + init). |
| rnc_yrs_store.d.ts | Types for the glue (referenced via @ts-self-types). |
| ../../../../../public/rnc-wasm/rnc_yrs_store_bg.wasm | The compiled engine, served as a static asset at /rnc-wasm/rnc_yrs_store_bg.wasm. |
How it was built
# from a checkout of Athena-Intel/spreadsheet @ branch bgeils/rnc-lazy-filter-derivation
# (private mirror of rowsncolumns/spreadsheet, based on feat/yrs-store-converged)
cd rust
wasm-pack build crates/rnc-yrs-store --target web --out-dir pkg-web --release
# → pkg-web/rnc_yrs_store.js, pkg-web/rnc_yrs_store.d.ts, pkg-web/rnc_yrs_store_bg.wasmThe fix branch bumps rnc-engine's formualizer-eval dep to 0.7 (from 0.5.9) — which
resolves the cold structural-recalc hang — and routes structural ops (insert/delete row/column)
on the capture-less CRDT store through a geometry-only fast path instead of an O(cells)
whole-document snapshot. Together these take an insert on a dense 100k+-cell sheet from a
multi-second (or unbounded) hang to sub-second.
The rnc-engine-collab-fixes branch builds on that and adds a remote-collab fix:
apply_remote_changes now registers a sheet a peer introduced via the merged CRDT update before
projecting its cells, so a remotely-authored formula enters the calc engine (it previously rendered
from cached values but never recomputed when a precedent changed).
The rnc-reload-facet-sync branch extends snapshot_commit (the reproject/reload render) to emit
the document facet planes — charts, pivots, conditional-formats, data-validations, citations —
mirroring load_document, so a peer's facet edit renders on another client's reload instead of
being dropped.
The rnc-remote-bulk-block-incremental branch makes a remote peer's cold-plane block change ingest
INCREMENTALLY — apply_remote_update re-loads only the touched bands via bulk_load_sheet_at
instead of rebuilding the whole engine (geometry shifts and block removals still fall back to a full
rebuild).
The rnc-capped-formulas-loading branch surfaces formulas the engine drops at ingest because their
row exceeds the 1,048,576-row calc-graph cap (rows are packed into 20 bits): they accumulate on the
workbook and drain into CommitResult.cappedFormulas so the host can warn the user instead of
silently losing them.
The rnc-lazy-filter-derivation branch makes basic-filter row visibility LAZY: apply_filter no
longer writes a hidden_by_filter flag per body row (an O(rows) write that bloats the doc ~50 MB at
1M rows), it persists only the spec's filterSpecs. read_window_commit (the windowed tile read)
and snapshot_commit (the full projection) DERIVE hidden_by_filter for just the rows they emit,
from the stored spec — so a million-row filter costs only the rows actually read, and every client
recomputes the same visibility (no per-row CRDT flags). delete_filter just clears the spec.
Because filter apply/delete is now spec-only (zero body cells touched), the basic-filter commands
(filter-table / create-basic-filter / delete-filter) join the structural-fast path on the
capture-less store: they emit full_resync_sheets (the host refetches tiles and re-derives
visibility) and skip the O(cells) snapshot/diff that otherwise hung the worker when a filter was
applied to a million-row windowed doc. snapshot_commit's full-projection derive is capped to a
non-windowed-scale body (windowed docs derive per tile), so a misrouted huge doc can't 1M-row hang.
The actual million-row filter "hang" was PER-ROW store reads in read_window_commit's filter
branch: the value-filter derivation called get_cell per body row (~512/tile) and the dims merge
called get_dimension per row — each opens a yrs read transaction on the 1M doc, so ~512–1024
per-row reads/tile cost ~12s and the grid's stream of tile reads saturated the single worker
thread, leaving the filter command queued forever. Fixes: read_window_commit now (a) derives
visibility from the cells it ALREADY fetched via one get_range (passes a (row,col)->text map into
the filter's value path instead of per-row get_cell; a tile spans all columns so a miss = empty
cell), and (b) emits only the derived hidden_by_filter flag with no per-row dim reads (persisted
heights/hiddenby_user fold in from the initial windowed load), and (c) evaluates simple comparison
CONDITIONS (NUMBER_, TEXT__, BLANK) directly in Rust against that same tile-cell text
(condition_matches_text) instead of eval_formula_truthy per row — the formula path loads a
calc-engine band per row it touches, so a million-row condition filter's full-doc scan otherwise
triggered ~hundreds of band loads (~seconds each). Aggregate/date/custom/list conditions (and the
full-projection path) still fall back to the formula engine. (Re-attaching a custom body-row height
under an active filter — one batched dim read — is a tracked follow-up.)
The rnc-filter-ranges-on-commit branch makes the host drive filter visibility from the engine's
COMPACT hidden-row ranges instead of a per-tile/per-row scan, fixing two ways the prior lazy approach
made a 1M filter feel broken. (1) document_facets_json exposes hiddenRowRanges per basic filter —
the contiguous [start,end] body rows the filter hides, from one filtered-column pass — read through a
per-sheet memo (filter_ranges_cache): computed on a miss, invalidated by apply_command only on a
filter command, a geometry shift, or an edit to a filtered column, and cleared on load / remote merge.
So the full scan runs ONCE per filter change, not on every facets refresh (which fires on every edit).
The host expands these ranges into the grid's rowMetadata.hiddenByFilter in one shot, so the grid has
the complete filtered layout without fetching every tile to learn it. (2) row_visible_under_filter
now treats an absent filtered cell as "" and condition_matches_text coerces a blank cell to 0 for
NUMBER_* (matching the formula engine), so a sparse/blank column under a simple condition decides on
the fast path instead of a per-row eval_formula_truthy band load (which hung the worker). (Driving the
grid's native visibleRowRanges directly — bypassing the per-row overlay entirely — is a tracked
follow-up that needs a vendored-CanvasGrid change.)
Local modification
rnc_yrs_store.js is generated, except for one deliberate edit: the default-init branch
module_or_path = new URL('rnc_yrs_store_bg.wasm', import.meta.url) is replaced with a
throw. webpack statically resolves new URL(<literal>, import.meta.url) at build
time and would fail to find the wasm from this src/ location, so callers MUST pass
an explicit path. use-rnc-engine.ts passes the public URL (/rnc-wasm/rnc_yrs_store_bg.wasm).
This directory is excluded from Biome (see olympus/biome.json); do not hand-format it.
To refresh, re-run the build above, re-copy the three files, and re-apply the one edit.
