Sheetwrite 0.5.0 adds an optional full formula engine, makes collaboration
recover without a reload, lets an app give engine memory back to the browser,
and makes large documents faster to load, edit, and undo. All seven packages
release as 0.5.0. Read the breaking changes first: most
apps already call initSheetwrite() and need no change.
Highlights
- Full formula engine.
@sheetwrite/formulasadds grouping, pivots,LAMBDA, statistics, regression, finance, regex, and reshaping functions. - Spill references and array constants in both engines:
=SUM(A1#)and={1,2;3,4}. - Large undo works. Undo of a very large clear is one history step, sent as one compressed operation or one atomic batch.
- Conflict recovery without a reload.
SyncCoordinator.recoverConflict()recovers a cell-level conflict on the live Grid. - Engine memory goes back to the browser.
releaseEngineMemory()moves the live stores to a fresh engine instance: after a million-row sheet is cleared to ten rows, WebAssembly memory drops from 173 MiB to 38 MiB. - String storage stays bounded. Text that edits overwrite or clear is reclaimed and reused instead of piling up for the life of the document.
- Faster. Loading 100,000 to 1,000,000 rows is 26–35% faster, XLSX import 38% faster, and CSV import 52% faster.
New
Full formula engine
@sheetwrite/formulas is the same Rust engine as @sheetwrite/wasm, built with
the analysis families. It has the same API and loaders. Select it one time,
before you create a Grid:
import * as formulas from "@sheetwrite/formulas";
It adds:
- grouping and pivot tables:
GROUPBY,PIVOTBY,PERCENTOF; LAMBDAand its helpers:MAP,REDUCE,SCAN,BYROW,BYCOL,MAKEARRAY,ISOMITTED;- 36 statistical distribution functions, descriptive statistics, and ranking;
- regression:
LINEST,LOGEST,TREND,GROWTH,FORECAST.LINEAR; - finance and working-day functions, including
NETWORKDAYS.INTL; - text:
TEXTSPLIT,TEXTBEFORE,TEXTAFTER, andREGEXTEST,REGEXEXTRACT,REGEXREPLACE; - reshaping:
VSTACK,HSTACK,WRAPROWS,TOCOL,EXPAND,SORTBY; - database functions and matrices:
MMULT,MINVERSE,MDETERM,MUNIT.
The default engine does not change: its function set and binary stay the same, and formula assist suggests only the functions of the selected engine.
- Analysis formulas guide: one worked task per family.
- Formula function contract: every function and its build.
- Formula analysis showcase: thirteen panels recalculate live over 20,000 orders.
Formulas in both engines
- Spill references.
=SUM(A1#)refers to the current spill ofA1and follows it when it changes size. XLSX files store it as_xlfn.ANCHORARRAY. See Spill references. - Array constants.
={1,2;3,4}spills two rows and two columns, and array constants work as function arguments. See Array constants. - Array arithmetic. Operators apply to each array element. Scalars and single rows or columns expand to match the other operand.
Large undo and atomic batches
Undo of a very large clear now works and stays one history step. The Grid sends
the restore as one compressed restoreBlock operation. If that still needs more
than one server version, it uses an atomic batch through the new optional
PersistenceAdapter.commitBatch: the server applies all versions or none, and
peers apply the whole batch in one transaction. An undo above every limit
changes nothing and emits mutation-rejected, and older edits stay undoable.
See Undo and redo and
Large undo and atomic batches.
Conflict recovery without a reload
After a base-version conflict, call sync.recoverConflict(). When the pending
work and the server versions it did not see change different cells, and neither
side inserts, deletes, or moves rows or columns, it applies the missed versions
to the live Grid, moves the pending queue onto the server head, and keeps undo
history. In every other case it returns reload-required with the reason and
changes nothing, so the host reloads as before. See
Conflict and snapshot reload.
Release engine memory
A WebAssembly instance never gives memory back, so after a large import or
clear the page used to keep its peak for its whole life. Call
releaseEngineMemory() when the app is idle. It waits for running
transactions and page loads, then moves every live store into a fresh engine
instance and lets the browser free the old one. Values, formulas and their
cached results, spills, loaded datasource pages, unsaved edits, and undo
history all move. If any store cannot move, nothing changes.
| After | WebAssembly memory | Pause |
|---|---|---|
| A million-row, three-column sheet is cleared to ten rows | 173 MiB → 38 MiB | about 50 ms |
| The same clear, with its undo history kept | 173 MiB → 76 MiB | about 270 ms |
| A paged store with unsaved edits | 38 MiB → 1.2 MiB | about 5 ms |
Release never makes memory larger, and the pause grows with the live data. See Release retained engine memory.
Other new APIs
| API | Package | Purpose |
|---|---|---|
grid.getTheme(), theme-change event | core | Read the base theme and react to setTheme or replaceTheme. |
maxRecentTransactions in createRowBridge | core | Size the row bridge's window of recent transaction IDs (default 8,192). |
SyncConflictRecovery, SyncConflictReloadReason, recovered event | core | Result and reasons of in-place conflict recovery. |
releaseEngineMemory(), EngineMemoryReleaseReport | core | Give engine memory back to the browser. |
createEngine(), EngineInstance | wasm, formulas | Create an independent engine instance from shared compiled code. |
setBlockPacked, setColumnBlockPacked, loadedSpans, cellSnapshots | wasm | Packed bulk writes and reads for direct engine users. |
formulaReadBands | wasm | The cells that visible formulas read, for windowed loading. |
stringPoolEpoch | wasm | The string-pool generation that raw string IDs belong to. |
Breaking changes
| Change | What to do |
|---|---|
Core needs initSheetwrite(). Core now owns its own engine instance, so calling only load() from @sheetwrite/wasm or @sheetwrite/formulas no longer lets you create a Grid or a SheetwriteStore; the error says so. | Call await initSheetwrite() before you create a Grid or store. Apps that already do need no change. |
A Grid with presentation: "data-grid" no longer adds empty, unnamed columns to fill its width. A spreadsheet still does. | Set minColumns if you want empty columns in a data grid. |
CellStore.setBlock is removed from @sheetwrite/wasm. | Use setBlockPacked (row-major) or setColumnBlockPacked (column-major). |
Raw string IDs from @sheetwrite/wasm can change when the string pool is rebuilt. | Drop cached string IDs when stringPoolEpoch changes. |
Changed
- String storage is reclaimed. Text that edits overwrite or clear was kept
for the life of the document. After a commit that leaves at least 256 KiB of
dead text, and more dead text than live text, the engine rebuilds its string
pool and reuses the freed space. History keeps its own text, so undo is not
affected. To shrink WebAssembly memory itself, use
releaseEngineMemory(). - One owner for formula results. The engine stores calculated values, spill output, and calculated errors apart from the cells you write. A write always wins over a calculated value, as before; spill reads and writes next to a spill are faster.
- Fill and Replace all send one operation. They used to send one
operation per cell, so the
maxOperationstransaction limit (10,000 by default) rejected a fill or replace over more than 10,000 cells. They now send one range operation, as paste already did, and only themaxEncodedByteslimit applies. Events, undo, and sync results are the same. - Resize previews. Row and column resize show a preview while you drag and record one undoable change on release, instead of changing the document on every step.
- Pointer gestures end with their context. A drag ends when the sheet, view, structure, or read-only state changes, and a second pointer cannot take over a drag.
- Selection before edit. Selection changes are reported before
edit-begin. - Negative currency. The minus sign comes before a currency prefix
(
-$246), as in Excel. - Smaller packages. Published packages no longer include source maps, and the React, Vue, and Svelte adapters declare only CSS files as side effects, so bundlers can remove unused JavaScript.
- New dependency.
@sheetwrite/coredepends onfflate0.8.3 for the restore codec.
Fixed
Formulas
- A spill that reads another spill shows its current values. Spills that read
each other show
#CYCLE!. - With a windowed datasource, a visible formula that reads an off-screen column
no longer stays
#LOADING!. - With a windowed datasource, a visible formula that reads a frozen row or a merged cell no longer shows a loading marker or a stale value, and the cells visible formulas read stay loaded while they are on screen.
- Exports,
getRange, and find no longer unload cells that are on screen. FILTERworks in debug builds when its input is smaller than its size bound.- Decoded lookup ranges are released when recalculation ends.
Collaboration and undo
rebaseDocumentOperationsno longer puts an edit on the wrong row when pending work holds more than one operation and one side inserted or deleted rows. An edit to a row the server deleted now returns a conflict instead of being written onto another row.- Undo after a collaborator inserts or deletes rows or columns restores the cell you edited, instead of writing the old value onto another row.
- An undo above the transaction limits now emits
mutation-rejectedand leaves older edits undoable, instead of failing silently every time. MemoryPersistenceAdapterworks before the engine is initialized, and a failed commit, including one member of an atomic batch, leaves the document unchanged.
Editing and display
- In the Svelte adapter, a live option such as
readOnlychanged while the Grid starts is applied beforereadyfires. In every adapter, a ref oronReadycallback that throws now retires the new Grid, as React already did. - Every editor commits to the cell it opened on, through sorting and filtering.
- A rejected fill leaves the selection unchanged.
- Paste across hidden columns writes only the visible columns, and one undo reverts it.
- The toolbar, find bar, and context menu follow a theme change, and the toolbar colour swatches show the theme's real colours.
- Row numbers no longer paint into the header corner while you scroll.
- Sheet tabs no longer scroll the page, and the sheet menu stays open when an unrelated part of the page scrolls.
- A search highlight or note on a partly visible top row is no longer dropped.
Performance
Numbers are medians from alternating runs on the same machine; results do not change. The performance page has the full measurements and the package sizes of both engines.
| Workload | 0.4.0 → 0.5.0 |
|---|---|
| Load 100,000 to 1,000,000 rows | 26–35% faster |
| Load number-only columns | 56–59% faster |
| Write a 1,000,000-cell block | 1,364 ms → 545 ms (60% faster) |
| Insert 10,000 rows into 1,000,000 with a row bridge | 34.7 s → 5 ms |
| Change one constant among 100,000 formulas | 35 ms → 0.03 ms |
SUMPRODUCT, COUNTIF, SUMIFS, PERCENTILE over ranges | 27–59% faster |
LET(x, lookup, x+x+x) | 87% faster |
| Value-list filter, 1,000 picks over 1,000,000 rows | 2.9 s → 28 ms |
| Distinct values | 25–85% faster |
| Multi-key sort | 34–38% faster |
| XLSX import, 1 MB workbook | 1,675 ms → 1,039 ms (38% faster) |
| XLSX import peak memory | 476 MiB → 312 MiB (34% lower) |
| CSV import, 200,000 × 10 | 493 ms → 239 ms (52% faster) |
| Scroll with wrapped text | 39% faster |
| Scroll with number formats | 14% faster |
| Restore 4,000,000 cells | 2,213 ms → 1,346 ms (39% faster) |
| Undo a 1,000,000-cell clear | 2,536 ms → 1,833 ms (28% faster) |
| Row resize | 0.218 ms → 0.144 ms (34% faster) |
| Framework adapter mount (React, Vue, Svelte) | 16–38% faster mount and reset |
One-cell commit to a 110,000-cell MemoryPersistenceAdapter document | ~450 ms → no full rebuild |
| Read a sheet with a spill | 32% faster |
| Write next to a spill | 61% faster |
| Fill 10,000 to 100,000 cells | 66–67% faster |
| Replace all, 10,000 to 100,000 matches | 49–50% faster |
| Delete a 100,000-cell sheet | 85% faster |
| Toolbar state for a 4,096-cell selection | 52% faster |
| String-pool storage after 100,000 unique overwrites (not WebAssembly heap size) | 84.3 MB → 0.35 MB (99.6% lower) |
Two operations are slower in 0.5.0. Both are tracked for a later release.
| Workload | Change |
|---|---|
Column sum (aggregate), 100,000 to 1,000,000 rows | About 2× slower than 0.4.0: 0.21 ms → 0.44 ms at 100,000 rows, 1.9 ms → 4.2 ms at 1,000,000. Fix planned for 0.5.1 (#110). |
| Create a 100,000-cell spill | 6–27% slower than before formula results moved out of the cells: 10.6 ms → 11.2–13.5 ms. Tracked in #109. |
Upgrade checklist
| You | Do this |
|---|---|
Use Sheetwrite through @sheetwrite/core or an adapter | Make sure you await initSheetwrite() before you create a Grid or store. Upgrade all @sheetwrite/* packages to 0.5.0 together. |
Use presentation: "data-grid" and rely on filler columns | Set minColumns. |
| Hold large documents for a long time | Optional: call releaseEngineMemory() when idle after a large clear or import. |
| Want the analysis functions | Install @sheetwrite/formulas and select it before you create a Grid. |
| Run a persistence or collaboration server | Accept restoreBlock operations before you accept 0.5.0 clients. Optional: implement commitBatch for undo that needs more than one version. See Large undo and atomic batches. |
| Recover from sync conflicts | Optional: call sync.recoverConflict() before your reload path. See Conflict and snapshot reload. |
Call CellStore.setBlock from @sheetwrite/wasm directly | Breaking. Use setBlockPacked (row-major) or setColumnBlockPacked (column-major). They take all string cells as one UTF-8 buffer plus byte offsets. |
Cache raw string IDs from @sheetwrite/wasm | Drop the cache when stringPoolEpoch changes. |
| Check dependency licenses or lockfiles | @sheetwrite/core now depends on fflate 0.8.3. Your package manager installs it; you do not add it yourself. |
The full list of changes is in each package's CHANGELOG.md.