Skip to content
Sheetwrite

Sheetwrite 0.5.0

The full formula engine, spill references, large undo, in-place conflict recovery, engine memory release, bounded string storage, faster loading and import, breaking changes, every fix, and the upgrade checklist for Sheetwrite 0.5.0.
On this page

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/formulas adds 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:

Select the full engine
import { initSheetwrite } from "@sheetwrite/core";
import * as formulas from "@sheetwrite/formulas";
await initSheetwrite(undefined, formulas);

It adds:

  • grouping and pivot tables: GROUPBY, PIVOTBY, PERCENTOF;
  • LAMBDA and 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, and REGEXTEST, 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.

Formulas in both engines

  • Spill references. =SUM(A1#) refers to the current spill of A1 and 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.

AfterWebAssembly memoryPause
A million-row, three-column sheet is cleared to ten rows173 MiB → 38 MiBabout 50 ms
The same clear, with its undo history kept173 MiB → 76 MiBabout 270 ms
A paged store with unsaved edits38 MiB → 1.2 MiBabout 5 ms

Release never makes memory larger, and the pause grows with the live data. See Release retained engine memory.

Other new APIs

APIPackagePurpose
grid.getTheme(), theme-change eventcoreRead the base theme and react to setTheme or replaceTheme.
maxRecentTransactions in createRowBridgecoreSize the row bridge's window of recent transaction IDs (default 8,192).
SyncConflictRecovery, SyncConflictReloadReason, recovered eventcoreResult and reasons of in-place conflict recovery.
releaseEngineMemory(), EngineMemoryReleaseReportcoreGive engine memory back to the browser.
createEngine(), EngineInstancewasm, formulasCreate an independent engine instance from shared compiled code.
setBlockPacked, setColumnBlockPacked, loadedSpans, cellSnapshotswasmPacked bulk writes and reads for direct engine users.
formulaReadBandswasmThe cells that visible formulas read, for windowed loading.
stringPoolEpochwasmThe string-pool generation that raw string IDs belong to.

Breaking changes

ChangeWhat 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 maxOperations transaction 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 the maxEncodedBytes limit 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/core depends on fflate 0.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.
  • FILTER works in debug builds when its input is smaller than its size bound.
  • Decoded lookup ranges are released when recalculation ends.

Collaboration and undo

  • rebaseDocumentOperations no 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-rejected and leaves older edits undoable, instead of failing silently every time.
  • MemoryPersistenceAdapter works 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 readOnly changed while the Grid starts is applied before ready fires. In every adapter, a ref or onReady callback 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.

Workload0.4.0 → 0.5.0
Load 100,000 to 1,000,000 rows26–35% faster
Load number-only columns56–59% faster
Write a 1,000,000-cell block1,364 ms → 545 ms (60% faster)
Insert 10,000 rows into 1,000,000 with a row bridge34.7 s → 5 ms
Change one constant among 100,000 formulas35 ms → 0.03 ms
SUMPRODUCT, COUNTIF, SUMIFS, PERCENTILE over ranges27–59% faster
LET(x, lookup, x+x+x)87% faster
Value-list filter, 1,000 picks over 1,000,000 rows2.9 s → 28 ms
Distinct values25–85% faster
Multi-key sort34–38% faster
XLSX import, 1 MB workbook1,675 ms → 1,039 ms (38% faster)
XLSX import peak memory476 MiB → 312 MiB (34% lower)
CSV import, 200,000 × 10493 ms → 239 ms (52% faster)
Scroll with wrapped text39% faster
Scroll with number formats14% faster
Restore 4,000,000 cells2,213 ms → 1,346 ms (39% faster)
Undo a 1,000,000-cell clear2,536 ms → 1,833 ms (28% faster)
Row resize0.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 spill32% faster
Write next to a spill61% faster
Fill 10,000 to 100,000 cells66–67% faster
Replace all, 10,000 to 100,000 matches49–50% faster
Delete a 100,000-cell sheet85% faster
Toolbar state for a 4,096-cell selection52% 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.

WorkloadChange
Column sum (aggregate), 100,000 to 1,000,000 rowsAbout 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 spill6–27% slower than before formula results moved out of the cells: 10.6 ms → 11.2–13.5 ms. Tracked in #109.

Upgrade checklist

YouDo this
Use Sheetwrite through @sheetwrite/core or an adapterMake 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 columnsSet minColumns.
Hold large documents for a long timeOptional: call releaseEngineMemory() when idle after a large clear or import.
Want the analysis functionsInstall @sheetwrite/formulas and select it before you create a Grid.
Run a persistence or collaboration serverAccept 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 conflictsOptional: call sync.recoverConflict() before your reload path. See Conflict and snapshot reload.
Call CellStore.setBlock from @sheetwrite/wasm directlyBreaking. 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/wasmDrop 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.

Documentation