Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/calm-tables-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sheetwrite/core": patch
---

Share cell-kind wire tags across the store, window reader, and paint worker to prevent internal WASM contract divergence.
12 changes: 6 additions & 6 deletions packages/core/src/store/data-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,12 @@ import { StoreSnapshotCodec } from "./snapshot-codec.js";
import { StoreViewState } from "./view-state.js";
import type { RecomputingCellStore } from "./wasm-contract.js";
import { StoreWindowReader } from "./window-reader.js";

// Mirror of the WASM cell tags.
const KIND_NUMBER = 1;
const KIND_STRING = 2;
const KIND_BOOL = 3;
const KIND_FORMULA = 4;
/**
* Cell kinds cross the WASM boundary in bulk paths here.
* Shared tags keep these encodings aligned with window reads
* and the paint worker.
*/
import { KIND_BOOL, KIND_FORMULA, KIND_NUMBER, KIND_STRING } from "./wire-tags.js";

const AGG_OP: Record<AggregateOp, number> = { sum: 0, avg: 1, min: 2, max: 3, count: 4 };

Expand Down
13 changes: 6 additions & 7 deletions packages/core/src/store/window-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import type { SheetId } from "../types/coordinates.js";
import type { Workbook } from "../types/document.js";
import type { ResourceOwnerBytes, VisibleWindowView } from "../types/store.js";
import type { ConsumingWindowView, RecomputingCellStore } from "./wasm-contract.js";
import { KIND_BOOL, KIND_NUMBER, KIND_STRING, NO_STRING } from "./wire-tags.js";

const KIND_NUMBER = 1;
const KIND_STRING = 2;
const KIND_BOOL = 3;
const EMPTY_COND_MATCHES = new Uint32Array(0);
// Kept independent from the worker cache cap so each thread can be tuned separately.
const STRING_CACHE_CAP = 65_536;
const WINDOW_SCRATCH_MAX_REUSE = 65_536;
const CONDITIONAL_MASK_BITS = Uint32Array.BYTES_PER_ELEMENT * 8;
Expand Down Expand Up @@ -264,7 +263,7 @@ export class StoreWindowReader {

for (let i = 0; i < stringIds.length; i++) {
const id = stringIds[i];
if (id !== undefined && id !== 0xffffffff && !this.stringCache.has(id)) {
if (id !== undefined && id !== NO_STRING && !this.stringCache.has(id)) {
if (!missingIdSet) missingIdSet = new Set<number>();
missingIdSet.add(id);
}
Expand All @@ -284,7 +283,7 @@ export class StoreWindowReader {
poolStringBytes,
);
for (let i = 0; i < stringPoolUpdateValues.length; i++) {
this.stringCache.set(stringPoolUpdateIds[i] ?? 0xffffffff, stringPoolUpdateValues[i] ?? "");
this.stringCache.set(stringPoolUpdateIds[i] ?? NO_STRING, stringPoolUpdateValues[i] ?? "");
}
}

Expand All @@ -295,8 +294,8 @@ export class StoreWindowReader {
} else if (kinds[i] === KIND_BOOL) {
values[i] = (numbers[i] ?? 0) !== 0;
} else if (kinds[i] === KIND_STRING) {
const poolId = stringIds[i] ?? 0xffffffff;
if (poolId !== 0xffffffff) {
const poolId = stringIds[i] ?? NO_STRING;
if (poolId !== NO_STRING) {
values[i] = this.stringCache.get(poolId) ?? null;
} else {
const stringSlot = stringIndex[i] ?? -1;
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/store/wire-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// These values mirror packages/wasm/src/types.rs and must change together.
export const KIND_EMPTY = 0 as const;
export const KIND_NUMBER = 1 as const;
export const KIND_STRING = 2 as const;
export const KIND_BOOL = 3 as const;
export const KIND_FORMULA = 4 as const;

export const NO_STRING = 0xffffffff as const;

export type CellKindTag =
| typeof KIND_EMPTY
| typeof KIND_NUMBER
| typeof KIND_STRING
| typeof KIND_BOOL
| typeof KIND_FORMULA;
6 changes: 3 additions & 3 deletions packages/core/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// busy main thread can't stall scrolling. Custom (function) cell renderers do
// not cross the worker boundary, so the registry here is always empty.
import { blitVerticalScroll, paintFrame, paintFreezeDivider } from "./canvas-paint.js";
import { KIND_NUMBER, KIND_STRING, NO_STRING } from "./store/wire-tags.js";
import type { CellScalar } from "./types/cell.js";
import type { CellRenderer, RenderLayout, Theme, Viewport } from "./types/render.js";
import type { VisibleWindowView } from "./types/store.js";
Expand Down Expand Up @@ -85,16 +86,15 @@ type WorkerMessage =
| { type: "destroy" };

const NO_RENDERERS: ReadonlyMap<string, CellRenderer> = new Map();
const KIND_NUMBER = 1;
const KIND_STRING = 2;
const NO_STRING = 0xffffffff;

/**
* Hard cap on the pool-id→string cache, mirroring `SheetwriteStore`. A full-sheet
* sweep would otherwise grow it to O(distinct strings), duplicating the WASM
* string pool on the JS heap. At the cap we drop it wholesale and re-warm from
* this frame's `stringPoolUpdate*` payload — cheap and self-healing.
*/
// Kept independent from the host reader's cache cap:
// each thread can be tuned separately.
const STRING_CACHE_CAP = 65_536;

interface WorkerRuntimeState {
Expand Down
42 changes: 42 additions & 0 deletions packages/core/test/wire-tags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { initSheetwrite } from "../src/grid.js";
import { KIND_BOOL, KIND_NUMBER, KIND_STRING, NO_STRING } from "../src/store/wire-tags.js";
import { SheetwriteStore } from "../src/store.js";
import { makeWorkbook } from "./fixtures.js";

beforeAll(async () => {
await initSheetwrite();
});

describe("cell-kind wire tags", () => {
it("matches the tags and string sentinel produced by the WASM store", () => {
const store = new SheetwriteStore(makeWorkbook(1));
try {
store.applyTransaction({
patches: [
{
op: "set",
addr: { sheet: "s1", row: 0, col: 0 },
value: { kind: "literal", value: 42 },
},
{
op: "set",
addr: { sheet: "s1", row: 0, col: 1 },
value: { kind: "literal", value: "alpha" },
},
{
op: "set",
addr: { sheet: "s1", row: 0, col: 2 },
value: { kind: "literal", value: true },
},
],
});

const view = store.getVisibleWindow("s1", { start: 0, end: 1 }, [0, 1, 2]);
expect(Array.from(view.valueKinds ?? [])).toEqual([KIND_NUMBER, KIND_STRING, KIND_BOOL]);
expect(view.stringPoolIds?.[0]).toBe(NO_STRING);
} finally {
store.dispose();
}
});
});
2 changes: 2 additions & 0 deletions packages/wasm/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::eval::expand_let_reachable_ast;
use crate::memory::MemoryOwnerStats;
use std::rc::Rc;

// TypeScript mirrors these values in packages/core/src/store/wire-tags.ts;
// packages/core/test/wire-tags.test.ts enforces the pairing.
pub(crate) const KIND_EMPTY: u8 = 0;
pub(crate) const KIND_NUMBER: u8 = 1;
pub(crate) const KIND_STRING: u8 = 2;
Expand Down
2 changes: 2 additions & 0 deletions scripts/check-import-cycles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ const STORE_EDGES: Readonly<Record<string, readonly string[]>> = {
"../types/store.ts",
],
"wasm-contract.ts": [],
"wire-tags.ts": [],
"window-reader.ts": [
"../style-dictionary.ts",
"../types/cell.ts",
"../types/coordinates.ts",
"../types/document.ts",
"../types/store.ts",
"wasm-contract.ts",
"wire-tags.ts",
],
};

Expand Down
3 changes: 2 additions & 1 deletion scripts/coverage-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,8 @@
"packages/core/src/store/ranges.ts",
"packages/core/src/store/snapshot-codec.ts",
"packages/core/src/store/view-state.ts",
"packages/core/src/store/window-reader.ts"
"packages/core/src/store/window-reader.ts",
"packages/core/src/store/wire-tags.ts"
],
"language": "typescript",
"tier": "B",
Expand Down
Loading