-
Notifications
You must be signed in to change notification settings - Fork 0
ARCHITECTURE
Status: v1.0.0 — TS/JS core complete; PHP and Go data-layer ports shipped; Rust not started License: Apache 2.0 Companion projects: LombokCharts, LombokCSS
Honesty note on scope. This document lays out the full target architecture for a Table + Spreadsheet library ported across TypeScript, PHP, Go, and Rust, published to npm/Packagist/Docker/etc. That is a multi-quarter effort for a real team. What ships alongside this doc is: (1) this design, (2) a working TypeScript/JavaScript core implementation with real tests, and (3) scaffolding (package manifests, CI, Dockerfile) for the other targets — not yet full ports. No software can honestly claim "audited, zero bugs" on day one; what we can do is build in the practices (typed core, fuzz/property tests, CI gates, semver, changelog) that make that claim earn itself over time.
- Render tabular data as a Table (read-focused, virtualized) and a Spreadsheet (editable grid, formulas, cell references) from the same underlying data model.
- Import/export: CSV, TSV, JSON, XLSX, ODS (stretch), Markdown table, HTML table.
- Split a sheet (by rows, by columns, by sheet/tab) and merge multiple sheets/tables into one, with conflict resolution rules.
- Templating system: named layout + style presets (e.g.
invoice,report,financial-statement,plain) that can be applied to a dataset without rewriting data. - Internationalization: number/date/currency formatting and UI strings for the world's major languages (start: en, es, zh, hi, ar, pt, fr, ru, ja, de, id), including RTL layout.
- Framework adapters: vanilla JS, React, Vue, (later) Svelte — thin wrappers around a framework-agnostic core, not reimplementations.
- Optional charting via LombokCharts and styling via LombokCSS as peer dependencies, never hard dependencies.
- Performance: virtualized rendering, target 100k rows / 50 columns interactive scroll at 60fps on mid-range hardware; O(1) amortized cell read/write.
- Bundle size: core < 15KB gzipped (no framework), adapters add < 3KB each.
-
Portability: core logic (data model, formula engine, import/export parsers)
written so it can be mechanically ported — no reliance on JS-only runtime quirks
(no
Proxy-only tricks in the algorithmic core, no reliance on GC timing). - License cleanliness: Apache 2.0 throughout; every dependency vetted for license compatibility (no GPL/AGPL transitive deps in the core).
-
Security: no
eval, no dynamicFunction()construction for formulas (use a parsed AST evaluator instead) — this is the #1 historical vuln class in spreadsheet libs.
- Single maintainer team at project start → phased roadmap, not simultaneous ports.
- Must interoperate with existing Lombok ecosystem (Charts, CSS) without tight coupling.
┌─────────────────────────────┐
│ Public API │
│ Table() / Spreadsheet() │
└──────────────┬────────────────┘
│
┌────────────────────────────────────────────────────┐
│ Core Engine (portable) │
│ ┌───────────┐ ┌────────────┐ ┌───────────────────┐ │
│ │ DataModel │ │ FormulaAST │ │ Renderer contract │ │
│ │ (grid, │ │ Evaluator │ │ (virtual DOM diff, │ │
│ │ sheets) │ │ (no eval) │ │ framework-agnostic│ │
│ └───────────┘ └────────────┘ └───────────────────┘ │
│ ┌────────────────────┐ ┌──────────────────────────┐│
│ │ Import/Export │ │ Template Registry ││
│ │ CSV/TSV/JSON/XLSX/ │ │ (JSON schema + CSS hooks)││
│ │ MD/HTML │ │ ││
│ └────────────────────┘ └──────────────────────────┘│
│ ┌────────────────────┐ ┌──────────────────────────┐│
│ │ Split/Merge engine │ │ i18n (ICU MessageFormat, ││
│ │ │ │ number/date/currency) ││
│ └────────────────────┘ └──────────────────────────┘│
└────────────────────────────────────────────────────┘
│
┌───────────────┬───────────────┼───────────────┬───────────────┐
│ Adapter: DOM │ Adapter: React │ Adapter: Vue │ Adapter: CLI │
└───────────────┴───────────────┴───────────────┴───────────────┘
│
┌──────────────────────────────┐
│ Optional peers: LombokCharts, │
│ LombokCSS │
└──────────────────────────────┘
- Data enters via Import (file/stream/array) or is constructed programmatically → normalized into
DataModel. -
DataModelis the single source of truth: a sparse 2D grid + metadata (column types, merges, styles-by-reference, sheet list). -
RendererreadsDataModel+ activeTemplateand produces a render tree; the DOM/React/Vue adapter is the only layer that touches the actual UI runtime. - Edits (Spreadsheet mode) go through a
Transactionobject (undo/redo, dirty-cell tracking) before mutatingDataModel, so formula recalculation can run a minimal dependency-graph update. -
Export reads
DataModel(+ optional template) and serializes to the target format.
Everything inside "Core Engine" is deliberately designed with no host-language-specific tricks: plain structs/records, explicit state transitions, no reflection-based magic. That's what makes a later Go or Rust port a translation exercise instead of a redesign. The only thing that changes per language port is the Renderer contract's concrete implementation and the adapter layer.
interface CellRef { row: number; col: number; sheet: string }
interface Cell {
value: string | number | boolean | null;
formula?: string; // raw formula text, e.g. "=SUM(A1:A10)"
type: 'string'|'number'|'boolean'|'date'|'formula'|'empty';
styleRef?: string; // points into StyleTable, never inline style blobs
}
interface Sheet {
name: string;
cells: Map<string, Cell>; // sparse: key = "row:col"
rowCount: number; colCount: number;
merges: Array<{from: CellRef; to: CellRef}>;
colTypes?: Record<number, ColumnType>;
}
interface Workbook {
sheets: Sheet[];
styles: StyleTable;
locale: string; // BCP-47, e.g. "id-ID", "ar-EG"
meta: { createdWith: 'LombokTableSheet'; version: string };
}Sparse storage keeps memory bounded for large-but-mostly-empty sheets; dense columns (typed, uniform) can opt into a columnar typed-array backing for numeric performance.
// Table (read-optimized)
const table = new LombokTable(container, { data, columns, template: 'report', locale: 'en' });
table.setData(newRows);
table.exportAs('csv' | 'xlsx' | 'json' | 'md' | 'html');
// Spreadsheet (editable)
const sheet = new LombokSheet(container, { workbook, locale: 'ja' });
sheet.on('cellChange', (ref, cell) => {...});
sheet.undo(); sheet.redo();
sheet.split({ by: 'rows', at: 42 }); // -> [Workbook, Workbook]
LombokSheet.merge([wbA, wbB], { onConflict: 'left-wins' | 'right-wins' | 'prompt' });- Tokenizer → Pratt-parser → AST → dependency graph (per cell) → topological recalculation.
- No
eval/new Function. Function library (SUM, AVG, IF, VLOOKUP-equivalent, etc.) registered as pure functions in a lookup table — also makes sandboxing trivial and the same table structure ports 1:1 to Go/Rust (map of function name → implementation). - Circular reference detection via cycle check on the dependency graph before commit.
- Each format is a
Codecwithdecode(bytes) -> Workbookandencode(Workbook) -> bytes. - XLSX via a minimal OOXML zip/XML reader-writer (no full Excel feature parity claimed — documented subset: values, basic styles, merged cells, sheet names).
- Split/Merge reuse the Codec's
Workbookshape, so they're format-agnostic.
Templates are JSON (layout regions, column width hints, style-ref defaults, header/footer slots) + a CSS hook namespace consumed by LombokCSS if present, or a small built-in stylesheet if not. Templates never touch data — purely presentational, so exporting to CSV/JSON strips template concerns entirely (as it should).
- Uses
Intl.NumberFormat/Intl.DateTimeFormaton the JS side; ICU4X-equivalent on Rust;golang.org/x/texton Go;intlext on PHP. UI strings via ICU MessageFormat catalogs insrc/i18n/<locale>.json, pluralization-aware. - RTL detected from locale and applied at the adapter/CSS layer, not baked into DataModel.
- Import errors are
Result<Workbook, ImportError[]>(never throw-and-lose-partial-data); partial imports return best-effort data + a warnings array. - Formula errors resolve to a typed error cell value (
#REF!,#DIV/0!,#CIRC!) rather than throwing, matching user expectations from Excel/Sheets/Calc.
- Load estimation: target use is embedded in web apps and CLIs, not a multi-tenant service — so "scale" here means client-side data volume, not request throughput.
- Virtualized rendering (windowing) keeps DOM node count bounded regardless of row count.
- Large-file import/export streams row-by-row rather than materializing the whole file in memory where the format allows it (CSV/TSV always; XLSX partially, due to zip format).
- No server component in v1 — reliability concerns are: don't corrupt data on crash-mid-edit (transaction log + autosave hook exposed to host app) and don't freeze the UI thread (heavy parse/recalc offloaded to a Web Worker where available).
| Decision | Chosen | Alternative | Trade-off |
|---|---|---|---|
| Formula engine | Custom AST evaluator |
eval/Function ctor |
Slower to write, but no code-injection surface — non-negotiable for a library with "audit, no bug" as a goal |
| Storage | Sparse map + optional typed-array | Dense 2D array always | Better memory for sparse sheets; slightly more complex code path |
| Core language | TypeScript (transpiled to plain JS + .d.ts) |
Rust+WASM as universal core | TS ships smaller/faster for pure web use today; Rust/WASM revisited once perf data justifies it (tracked in §7) |
| Porting strategy | Sequential (TS → PHP → Go → Rust) | Parallel from day one | Sequential lets each port learn from the last language's edge cases instead of triplicating the same bugs |
| Templates | Pure JSON + CSS hooks | Component-based templates | JSON is portable across every target language; component templates would lock the templating system to JS |
What we'd revisit as the project grows: if profiling shows the JS core is the bottleneck for very large in-browser sheets, promote the recalculation engine to a Rust/WASM module behind the same TS interface (the AST evaluator's pure-function design makes this a drop-in swap, not a rewrite).
"No bug" cannot be promised on day one by any team; the honest version of this goal is a process. See SECURITY.md for the running, honest record of that process in practice — including two real bugs the fuzz test already found and fixed. Summary of the commitments:
- Core has no
eval, no dynamic code execution, no unsanitized HTML injection in the DOM adapter (all cell content text-noded, not innerHTML'd). - 100% of
DataModelmutations go through theTransactionlayer → property-based tests (fuzz random transaction sequences, assert invariants never break). - CI gate: unit tests + property tests + a static analysis pass (
tsc --strict, ESLint security ruleset) must pass before merge tomain. - Public
SECURITY.mdwith a disclosure process before v1.0. - Dependency license + vulnerability scan (
npm audit/cargo audit/composer audit) in CI, blocking on high-severity findings.
| Stage | Deliverable | Target |
|---|---|---|
| 0 | This architecture doc | Done (this doc) |
| 1 | TS/JS core: DataModel, CSV/JSON codec, Table (read-only) render, unit tests | MVP — shipped alongside this doc |
| 2 | Spreadsheet edit mode, formula engine, undo/redo |
Done — LombokSheet, AST-based formula evaluator (no eval), TransactionalSheet undo/redo |
| 3 | XLSX/MD/HTML codecs, split/merge, templates v1, i18n (10 locales) |
Done — XLSX via a hand-written, dependency-free ZIP writer (validated against unzip, Python's zipfile, and independently against openpyxl); HTML codec; split/merge, templates, and i18n were already in place from Stage 1 |
| 4 | React + Vue adapters, LombokCharts/LombokCSS integration |
Done (React/Vue adapters) — thin wrappers as opt-in sub-path exports (lomboktablesheet/react, lomboktablesheet/vue), core bundle has zero React/Vue code. LombokCharts/LombokCSS integration remains hook-level only (cssHooks on templates) until those sibling projects exist to integrate against. |
| 5 | npm publish, unpkg CDN, GitHub Actions CI/CD, Docker demo image | +2 weeks |
| 6 | PHP port (Packagist/Composer) — reuses format specs & test fixtures from TS |
Done (data-layer scope) — ports/php/, core model + formula engine + CSV/JSON/Markdown + split/merge, 27 PHPUnit tests, verified byte-identical output vs. the TS core on matching inputs (CSV→JSON, split, and a compound formula). DOM/Sheet rendering, XLSX, i18n, templates intentionally out of scope for PHP (no DOM to render into — see ports/php/README.md). |
| 7 | Go port |
Done (data-layer scope, same as PHP) — ports/go/, 34 tests (83.2% coverage), go vet/gofmt clean, uses Go standard library (encoding/csv, encoding/json) rather than hand-rolling, verified three-way parity with TS and PHP on matching inputs |
| 8 | Rust port (+ WASM build, optional perf-core for TS) | +8–10 weeks |
| 9 | Independent security audit + fuzzing campaign before v1.0.0 | Before v1.0 |
See DEPLOYMENT.md for the concrete per-target deployment stages (GitHub, Docker, AWS,
Niagahoster/shared VPS, NPM, Packagist, unpkg, framework integration).
Each port isn't just "similar" — it's checked against the TS core (and against each other) with identical inputs, asserted to produce identical outputs:
| Check | TS result | PHP result | Go result |
|---|---|---|---|
decodeCsv → encodeJson on "name,age\nAlice,30\nBob,25\n"
|
[{"name":"Alice","age":30},{"name":"Bob","age":25}] |
identical, byte-for-byte | same key/value pairs (JSON key order differs — Go map iteration isn't ordered, which doesn't affect correctness) |
splitByRows(..., 2) on the same workbook |
[["name","age"],["Alice",30]] / [["Bob",25]]
|
identical | identical |
=SUM(A1:B1)*2+IF(A1>5,1,0) with A1=10, B1=20 |
61 |
61 |
61 |
This is the standard the remaining port (Rust) should be held to as it lands — a port is only "done" once it's cross-checked against the reference implementation on real inputs, not just once its own unit tests pass in isolation.
LombokTableSheet/
├── ARCHITECTURE.md ← this file
├── README.md ← standard, user-facing
├── DEPLOYMENT.md ← multi-stage deploy plan
├── LICENSE ← Apache-2.0
├── package.json
├── src/
│ ├── core/ ← DataModel, Transaction, FormulaAST
│ ├── formats/ ← csv.ts, json.ts, xlsx.ts, md.ts, html.ts
│ ├── templates/ ← built-in template JSON + registry
│ ├── i18n/ ← locale catalogs
│ ├── adapters/ ← dom.ts, react.tsx, vue.ts
│ └── index.ts
├── tests/
├── examples/{react,vue,vanilla}
├── docker/Dockerfile
├── .github/workflows/ci.yml
└── ports/
├── php/ ← done (Stage 6): src/Core, src/Formats, tests/, composer.json
└── go/ ← done (Stage 7): lombok/ package, go.mod, tests
rust/ to follow in its respective stage