From 9e088783504826afd422623d38b5ba1cdc1738f1 Mon Sep 17 00:00:00 2001 From: chh-ay Date: Sun, 26 Jul 2026 22:26:06 +0700 Subject: [PATCH 1/2] refactor(core): share WASM cell-kind wire tags --- .changeset/calm-tables-share.md | 5 +++ packages/core/src/store/data-engine.ts | 12 +++---- packages/core/src/store/window-reader.ts | 13 ++++---- packages/core/src/store/wire-tags.ts | 15 +++++++++ packages/core/src/worker.ts | 6 ++-- packages/core/test/wire-tags.test.ts | 42 ++++++++++++++++++++++++ packages/wasm/src/types.rs | 2 ++ scripts/check-import-cycles.ts | 2 ++ 8 files changed, 81 insertions(+), 16 deletions(-) create mode 100644 .changeset/calm-tables-share.md create mode 100644 packages/core/src/store/wire-tags.ts create mode 100644 packages/core/test/wire-tags.test.ts diff --git a/.changeset/calm-tables-share.md b/.changeset/calm-tables-share.md new file mode 100644 index 00000000..8e13a0ba --- /dev/null +++ b/.changeset/calm-tables-share.md @@ -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. diff --git a/packages/core/src/store/data-engine.ts b/packages/core/src/store/data-engine.ts index 34cdbb6e..35e8d403 100644 --- a/packages/core/src/store/data-engine.ts +++ b/packages/core/src/store/data-engine.ts @@ -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 = { sum: 0, avg: 1, min: 2, max: 3, count: 4 }; diff --git a/packages/core/src/store/window-reader.ts b/packages/core/src/store/window-reader.ts index 8ea2e7f1..1422f34d 100644 --- a/packages/core/src/store/window-reader.ts +++ b/packages/core/src/store/window-reader.ts @@ -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; @@ -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(); missingIdSet.add(id); } @@ -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] ?? ""); } } @@ -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; diff --git a/packages/core/src/store/wire-tags.ts b/packages/core/src/store/wire-tags.ts new file mode 100644 index 00000000..e9a007b2 --- /dev/null +++ b/packages/core/src/store/wire-tags.ts @@ -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; diff --git a/packages/core/src/worker.ts b/packages/core/src/worker.ts index ed58ecdc..96f5e1c6 100644 --- a/packages/core/src/worker.ts +++ b/packages/core/src/worker.ts @@ -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"; @@ -85,9 +86,6 @@ type WorkerMessage = | { type: "destroy" }; const NO_RENDERERS: ReadonlyMap = 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 @@ -95,6 +93,8 @@ const NO_STRING = 0xffffffff; * 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 { diff --git a/packages/core/test/wire-tags.test.ts b/packages/core/test/wire-tags.test.ts new file mode 100644 index 00000000..0ee80633 --- /dev/null +++ b/packages/core/test/wire-tags.test.ts @@ -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(); + } + }); +}); diff --git a/packages/wasm/src/types.rs b/packages/wasm/src/types.rs index 5240c809..413d0c29 100644 --- a/packages/wasm/src/types.rs +++ b/packages/wasm/src/types.rs @@ -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; diff --git a/scripts/check-import-cycles.ts b/scripts/check-import-cycles.ts index c8daaf38..83ceecc5 100644 --- a/scripts/check-import-cycles.ts +++ b/scripts/check-import-cycles.ts @@ -37,6 +37,7 @@ const STORE_EDGES: Readonly> = { "../types/store.ts", ], "wasm-contract.ts": [], + "wire-tags.ts": [], "window-reader.ts": [ "../style-dictionary.ts", "../types/cell.ts", @@ -44,6 +45,7 @@ const STORE_EDGES: Readonly> = { "../types/document.ts", "../types/store.ts", "wasm-contract.ts", + "wire-tags.ts", ], }; From 2667f30b6146900cb735b7021fa5065ff80b5669 Mon Sep 17 00:00:00 2001 From: chh-ay Date: Sun, 26 Jul 2026 22:43:21 +0700 Subject: [PATCH 2/2] test: classify wire tags for coverage --- scripts/coverage-thresholds.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/coverage-thresholds.json b/scripts/coverage-thresholds.json index 145f6147..d4e26d4e 100644 --- a/scripts/coverage-thresholds.json +++ b/scripts/coverage-thresholds.json @@ -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",