concertina 0.13.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -0
- package/dist/accordion.cjs +1 -0
- package/dist/accordion.js +1 -1
- package/dist/{chunk-OGJMPKZX.js → chunk-6UMIJ4S7.js} +1 -0
- package/dist/core/data-worker.js +550 -0
- package/dist/core/index.cjs +669 -0
- package/dist/core/index.d.cts +275 -0
- package/dist/core/index.d.ts +275 -0
- package/dist/core/index.js +637 -0
- package/dist/index.cjs +53 -58
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -39
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -465,6 +465,152 @@ The `inert` attribute shipped before `1lh` in every browser. No polyfills. No fa
|
|
|
465
465
|
|
|
466
466
|
These are proposals, not commitments. If any would unblock your project, open an issue.
|
|
467
467
|
|
|
468
|
+
---
|
|
469
|
+
|
|
470
|
+
## Core Stability Engine (`concertina/core`)
|
|
471
|
+
|
|
472
|
+
The Core Stability Engine is a high-performance sub-package for virtualizing large datasets. Import from the sub-path:
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
import {
|
|
476
|
+
useStabilityOrchestrator,
|
|
477
|
+
VirtualChamber,
|
|
478
|
+
createRecordBatchStream,
|
|
479
|
+
} from "concertina/core";
|
|
480
|
+
import type { RowProxy, ColumnSchema } from "concertina/core";
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
It runs all data work inside a dedicated Web Worker. The main thread receives only the rows currently visible on screen, as a single transferred `ArrayBuffer`. No data is ever copied; no JSON is ever parsed on the main thread.
|
|
484
|
+
|
|
485
|
+
### Architecture
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
Main thread DataWorker (off-thread)
|
|
489
|
+
────────────────────── ──────────────────────────────────
|
|
490
|
+
useStabilityOrchestrator Columnar storage
|
|
491
|
+
│ NumericColumn (f64/i32/u32/bool)
|
|
492
|
+
├─ ingest(stream) Utf8Column (offset + bytes)
|
|
493
|
+
│ └─ pump: one batch ─INGEST─▶ ListUtf8Column (3-level index)
|
|
494
|
+
│ await INGEST_ACK ◀──────── commit → INGEST_ACK
|
|
495
|
+
│ next batch → ...
|
|
496
|
+
│
|
|
497
|
+
├─ scroll → SET_WINDOW ──────▶ packWindowBuffer()
|
|
498
|
+
│ └─ single ArrayBuffer ─WINDOW_UPDATE─▶
|
|
499
|
+
│ ◀─ transferred
|
|
500
|
+
├─ rAF → FRAME_ACK ──────────▶ BackpressureController
|
|
501
|
+
│
|
|
502
|
+
└─ VirtualChamber
|
|
503
|
+
buildAccessors(buffer) ← reads transferred ArrayBuffer, zero-copy
|
|
504
|
+
buildRowProxy(accessors) ← column → scalar or string[]
|
|
505
|
+
pool nodes: constant DOM count, recycled by CSS transform
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### Binary wire format
|
|
509
|
+
|
|
510
|
+
All multi-byte values are little-endian. Every INGEST payload and every WINDOW_UPDATE payload uses this layout:
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
Header (16 bytes):
|
|
514
|
+
[0] u32 magic = 0xac1dc0de
|
|
515
|
+
[4] u32 seq monotonic batch sequence number
|
|
516
|
+
[8] u32 rowCount
|
|
517
|
+
[12] u32 colCount
|
|
518
|
+
|
|
519
|
+
Column Descriptors (colCount × 8 bytes):
|
|
520
|
+
[+0] u32 typeTag (0=f64, 1=i32, 2=u32, 3=bool, 4=timestamp_ms, 5=utf8, 6=list_utf8)
|
|
521
|
+
[+4] u32 byteLen byte length of this column's data block
|
|
522
|
+
|
|
523
|
+
Column Data Blocks (variable, one per column):
|
|
524
|
+
|
|
525
|
+
f64 / timestamp_ms rowCount × 8 bytes (Float64Array, little-endian)
|
|
526
|
+
i32 rowCount × 4 bytes (Int32Array)
|
|
527
|
+
u32 rowCount × 4 bytes (Uint32Array)
|
|
528
|
+
bool rowCount × 1 byte (Uint8Array, 0 or 1)
|
|
529
|
+
|
|
530
|
+
utf8 (rowCount+1) × 4 bytes Uint32 offsets (row i → bytes[offsets[i]..offsets[i+1]])
|
|
531
|
+
Σ(string lengths) bytes Uint8 data
|
|
532
|
+
|
|
533
|
+
list_utf8 4 bytes u32 totalItems
|
|
534
|
+
(rowCount+1) × 4 Uint32 rowOffsets
|
|
535
|
+
row i → items[rowOffsets[i]..rowOffsets[i+1])
|
|
536
|
+
(totalItems+1) × 4 Uint32 itemOffsets
|
|
537
|
+
item j → bytes[itemOffsets[j]..itemOffsets[j+1])
|
|
538
|
+
Σ(item byte lengths) Uint8 bytes (UTF-8)
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
`list_utf8` is a three-level nested index. The decoder in `VirtualChamber` walks:
|
|
542
|
+
1. `rowOffsets[localRow]..rowOffsets[localRow+1]` → item range for this row
|
|
543
|
+
2. `itemOffsets[j]..itemOffsets[j+1]` → byte range for item j
|
|
544
|
+
3. `TextDecoder.decode(bytes.subarray(...))` → string
|
|
545
|
+
|
|
546
|
+
`RowProxy.get()` returns `string[]` for `list_utf8` columns — no `JSON.parse` on the main thread.
|
|
547
|
+
|
|
548
|
+
### INGEST_ACK backpressure protocol
|
|
549
|
+
|
|
550
|
+
Without flow control, a 1M-row dataset would queue all batches in the IPC channel simultaneously (~300 MB). The INGEST_ACK loop bounds this to one batch in flight at a time:
|
|
551
|
+
|
|
552
|
+
```
|
|
553
|
+
Main thread pump DataWorker
|
|
554
|
+
───────────────── ──────────
|
|
555
|
+
read batch N from stream
|
|
556
|
+
register ackResolvers[N] = {resolve, reject}
|
|
557
|
+
postMessage(INGEST, [buffer], N) → parseBatch()
|
|
558
|
+
commit to columnar storage
|
|
559
|
+
emit INGEST_ACK(N)
|
|
560
|
+
resolve(ackResolvers[N]) ←
|
|
561
|
+
read batch N+1 from stream
|
|
562
|
+
...
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**IPC queue depth: O(1) regardless of dataset size.**
|
|
566
|
+
|
|
567
|
+
If the worker crashes (`onerror`), all pending `ackResolvers` are rejected immediately — the pump unblocks, and `store.setStatus("error")` is set. The pump does not zombie-wait.
|
|
568
|
+
|
|
569
|
+
### Supported column types
|
|
570
|
+
|
|
571
|
+
| Schema type | JS input value | RowProxy return type |
|
|
572
|
+
|----------------|----------------|----------------------|
|
|
573
|
+
| `f64` | `number` | `number` |
|
|
574
|
+
| `i32` | `number` | `number` |
|
|
575
|
+
| `u32` | `number` | `number` |
|
|
576
|
+
| `bool` | `boolean` | `boolean` |
|
|
577
|
+
| `timestamp_ms` | `number` (epoch ms) | `number` |
|
|
578
|
+
| `utf8` | `string` | `string` |
|
|
579
|
+
| `list_utf8` | `string[]` | `string[]` |
|
|
580
|
+
|
|
581
|
+
### Parallel list columns
|
|
582
|
+
|
|
583
|
+
For structs that require both an `id` and a `label` (e.g. `{ id: string; displayName: string }`), encode as two parallel `list_utf8` columns and zip them in `renderRow`:
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
// Schema
|
|
587
|
+
{ name: "organism_ids", type: "list_utf8", maxContentChars: 36 },
|
|
588
|
+
{ name: "organism_names", type: "list_utf8", maxContentChars: 80 },
|
|
589
|
+
|
|
590
|
+
// fileToRow
|
|
591
|
+
organism_ids: f.organisms.map(o => o.id),
|
|
592
|
+
organism_names: f.organisms.map(o => o.displayName),
|
|
593
|
+
|
|
594
|
+
// renderRow — O(k) zip, no JSON.parse
|
|
595
|
+
const ids = proxy.get("organism_ids") as string[];
|
|
596
|
+
const names = proxy.get("organism_names") as string[];
|
|
597
|
+
const orgs = ids.map((id, i) => ({ id, displayName: names[i] ?? "" }));
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
The DataWorker enforces that parallel columns maintain identical row counts after every batch commit. A count mismatch emits `INGEST_ERROR` and the pump is still ACK'd (so it does not stall), but the store transitions to the error state.
|
|
601
|
+
|
|
602
|
+
### Zero-Measurement layout
|
|
603
|
+
|
|
604
|
+
Column pixel widths are computed entirely in the worker from schema metadata — no DOM measurement ever happens:
|
|
605
|
+
|
|
606
|
+
```
|
|
607
|
+
computedWidth = fixedWidth ?? (maxContentChars × charWidthHint + CELL_H_PADDING × 2)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
`CELL_H_PADDING` is 16 px. A 14 px monospace font uses `charWidthHint: 8`. Widths are resolved once at `INIT` and re-sent with every `WINDOW_UPDATE`.
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
468
614
|
## License
|
|
469
615
|
|
|
470
616
|
MIT
|
package/dist/accordion.cjs
CHANGED
package/dist/accordion.js
CHANGED