@zakkster/lite-o1 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@zakkster/lite-o1` are documented here. The format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-09-15
8
+
9
+ Initial release. The headline member of the O(1) family, plus the analytical
10
+ anchor that proves the constant.
11
+
12
+ ### Added
13
+
14
+ - **`SparseSet(universe, capacity = universe)`** -- a zero-GC O(1) integer set
15
+ over `[0, universe)` (a dense + sparse `Uint32Array` pair):
16
+ - `add(k)` / `has(k)` / `delete(k)` / `clear()` / `forEach(fn)` /
17
+ `[Symbol.iterator]` -- all O(1) worst-case, zero allocation after
18
+ construction. `size` and `capacity` getters.
19
+ - `clear()` is O(1): resets the live count and zeroes NEITHER backing array.
20
+ The cross-checked membership invariant `sparse[k] < n && dense[sparse[k]] === k`
21
+ rejects stale sparse pointers.
22
+ - Fail closed: the constructor and `add` throw a `[lite-o1]`-tagged `RangeError`
23
+ on a non-integer / out-of-range key or when full; `has` / `delete` never throw
24
+ (a bad key is absent). `null` is not zero.
25
+ - **`VERSION`** const (`'0.1.0'`).
26
+ - **`O1.d.ts`** -- hand-written ambient types mirroring the runtime surface.
27
+ - **The O(1) Witness** (`test/witness.mjs`, `npm run witness`) -- throughput
28
+ invariance across an n-sweep `[1e3..1e7]` (batch 1e6, warm-up + median of 5)
29
+ with a native `Set` foil and a gated flatness floor (SparseSet `>= 0.70`,
30
+ foil `<= 0.55`, ratio `>= 1.5x`).
31
+ - **Torture gate** (`test/torture.mjs`, `npm run torture`) -- `@zakkster/lite-leak`
32
+ + `@zakkster/lite-gc-profiler`: 0 B/op on the hot path, 0 major GCs, leak-free
33
+ fill/clear cycles.
34
+ - **19 `node:test` cases** including a byte-identical `clear()` proof and a
35
+ 1,000,000-op differential fuzz against a `Set` oracle.
36
+
37
+ [0.1.0]: https://www.npmjs.com/package/@zakkster/lite-o1/v/0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/O1.d.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @zakkster/lite-o1 -- ambient type surface.
3
+ *
4
+ * Hand-written to mirror EXACTLY the runtime exports of O1.js. The three-place
5
+ * version sync (package.json / O1.js VERSION / llms.txt) is enforced in review;
6
+ * this file only declares that `VERSION` exists. ASCII-only.
7
+ *
8
+ * @license MIT
9
+ */
10
+
11
+ /** Package version string. */
12
+ export const VERSION: string;
13
+
14
+ /**
15
+ * A zero-GC O(1) integer set over the universe [0, universe), holding at most
16
+ * `capacity` live members. add / has / delete / clear / iterate are O(1)
17
+ * worst-case and allocate nothing after construction. A bad key (negative,
18
+ * fractional, NaN, null, >= universe) is absent for has/delete and throws for
19
+ * add. `clear()` is O(1) and touches neither backing array.
20
+ */
21
+ export class SparseSet {
22
+ /**
23
+ * @param universe exclusive key ceiling; an integer in [1, 2^32].
24
+ * @param capacity max live entries; an integer in [1, universe]. Defaults to universe.
25
+ */
26
+ constructor(universe: number, capacity?: number);
27
+
28
+ /** Number of live members. */
29
+ readonly size: number;
30
+
31
+ /** Max live members this set was sized for. */
32
+ readonly capacity: number;
33
+
34
+ /** True iff k is a present member. Never throws; a bad key is absent. */
35
+ has(k: number): boolean;
36
+
37
+ /** Add k (idempotent). Throws a [lite-o1] error on a bad key or when full. */
38
+ add(k: number): this;
39
+
40
+ /** Remove k. Returns true iff it was present. Never throws. */
41
+ delete(k: number): boolean;
42
+
43
+ /** Empty the set in O(1) (resets the count; zeroes no store). */
44
+ clear(): void;
45
+
46
+ /** Iterate present keys in insertion order, alloc-free. */
47
+ forEach(fn: (key: number, set: SparseSet) => void): void;
48
+
49
+ /** Iterate present keys in insertion order. */
50
+ [Symbol.iterator](): IterableIterator<number>;
51
+ }
package/O1.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @zakkster/lite-o1 -- a tree-shakeable, zero-GC family of O(1) data structures
3
+ * that doubles as a teachable textbook: each member solves a real problem AND
4
+ * proves its constant is real (the O(1) Witness -- see test/witness.mjs).
5
+ *
6
+ * v0.1.0 ships the headline member, SparseSet, plus its `VERSION` const.
7
+ *
8
+ * The complexity class IS the product: every hot op below is O(1) worst-case and
9
+ * allocates ZERO bytes after construction. The witness harness (never imported
10
+ * here) proves the throughput stays FLAT from n=1e3 to n=1e7 while a native Set
11
+ * decays -- that flat line is the theorem made visible.
12
+ *
13
+ * @license MIT
14
+ */
15
+
16
+ /** Package version. One of the three version sites (package.json / VERSION / llms.txt). */
17
+ export const VERSION = '0.1.0';
18
+
19
+ /** Largest universe the Uint32 substrate + the (k >>> 0) key check can honor. */
20
+ const MAX_UNIVERSE = 0x100000000; // 2^32
21
+
22
+ /**
23
+ * SparseSet -- a zero-GC O(1) integer set (a dense + sparse Uint32Array pair).
24
+ *
25
+ * add / has / delete / clear / iterate are ALL O(1) worst-case. Membership is a
26
+ * single cross-checked double indirection:
27
+ *
28
+ * sparse[k] < n && dense[sparse[k]] === k
29
+ *
30
+ * so `clear()` is O(1): it resets the live count and touches NEITHER array. A
31
+ * stale sparse entry left behind by a previous fill is ignored because the
32
+ * cross-check fails -- no store is ever zeroed. Iteration walks the dense prefix
33
+ * in insertion order, alloc-free.
34
+ *
35
+ * Universe is [0, universe); at most `capacity` entries are live at once. Fail
36
+ * closed: an out-of-range or non-integer key is ABSENT (has returns false, never
37
+ * throws); adding one, or adding past capacity, throws a [lite-o1]-tagged error.
38
+ * `null` is not zero -- (null >>> 0) === null is false, so null is rejected.
39
+ */
40
+ export class SparseSet {
41
+ /**
42
+ * @param {number} universe exclusive key ceiling; integer in [1, 2^32].
43
+ * @param {number} [capacity=universe] max live entries; integer in [1, universe].
44
+ */
45
+ constructor(universe, capacity = universe) {
46
+ if (!Number.isInteger(universe) || universe < 1 || universe > MAX_UNIVERSE) {
47
+ throw new RangeError(
48
+ '[lite-o1] universe must be an integer in [1, 2^32], got ' + universe);
49
+ }
50
+ if (!Number.isInteger(capacity) || capacity < 1 || capacity > universe) {
51
+ throw new RangeError(
52
+ '[lite-o1] capacity must be an integer in [1, ' + universe + '], got ' + capacity);
53
+ }
54
+ this._universe = universe;
55
+ this._cap = capacity;
56
+ this._dense = new Uint32Array(capacity); // dense[i] = the i-th member key
57
+ this._sparse = new Uint32Array(universe); // sparse[k] = index into _dense (valid iff cross-check holds)
58
+ this._n = 0;
59
+ }
60
+
61
+ /** Number of live members. O(1). */
62
+ get size() { return this._n; }
63
+
64
+ /** Max live members this set was sized for. O(1). */
65
+ get capacity() { return this._cap; }
66
+
67
+ /**
68
+ * True iff k is present. O(1): one branchless key check + one cross-checked
69
+ * indirection. A bad key (negative, fractional, NaN, null, Symbol, BigInt,
70
+ * >= universe) is ABSENT, never a throw and never slot 0. The `typeof`
71
+ * short-circuits BEFORE `>>>` runs, because `>>>` coerces its operand first
72
+ * and that coercion THROWS on a Symbol or BigInt; `(k >>> 0) !== k` then
73
+ * rejects every non-uint32 number in one test.
74
+ */
75
+ has(k) {
76
+ if (typeof k !== 'number' || (k >>> 0) !== k || k >= this._universe) return false;
77
+ const i = this._sparse[k];
78
+ return i < this._n && this._dense[i] === k;
79
+ }
80
+
81
+ /**
82
+ * Add k. O(1). Idempotent -- re-adding a present key is a no-op. Fails closed:
83
+ * a bad key throws via _oob; a new key when full throws via _full.
84
+ * @returns {SparseSet} this
85
+ */
86
+ add(k) {
87
+ if (typeof k !== 'number' || (k >>> 0) !== k || k >= this._universe) return this._oob(k);
88
+ const i = this._sparse[k];
89
+ if (i < this._n && this._dense[i] === k) return this; // already present
90
+ if (this._n === this._cap) return this._full();
91
+ const j = this._n++;
92
+ this._dense[j] = k;
93
+ this._sparse[k] = j;
94
+ return this;
95
+ }
96
+
97
+ /**
98
+ * Delete k by swapping the last dense entry into its slot and fixing that
99
+ * entry's back-pointer. O(1). A bad or absent key returns false (never throws).
100
+ * @returns {boolean} true iff k was present and removed.
101
+ */
102
+ delete(k) {
103
+ if (typeof k !== 'number' || (k >>> 0) !== k || k >= this._universe) return false;
104
+ const i = this._sparse[k];
105
+ if (i >= this._n || this._dense[i] !== k) return false;
106
+ const last = --this._n;
107
+ const moved = this._dense[last];
108
+ this._dense[i] = moved;
109
+ this._sparse[moved] = i;
110
+ return true;
111
+ }
112
+
113
+ /**
114
+ * Empty the set in O(1). Resets the live count only -- the dense and sparse
115
+ * stores are left BYTE-IDENTICAL; the stale sparse entries fail the has()
116
+ * cross-check, so they can never read as present.
117
+ */
118
+ clear() { this._n = 0; }
119
+
120
+ /**
121
+ * Iterate present keys in insertion order, alloc-free. O(size).
122
+ * @param {(key:number, set:SparseSet)=>void} fn
123
+ */
124
+ forEach(fn) {
125
+ const d = this._dense;
126
+ for (let i = 0; i < this._n; i++) fn(d[i], this);
127
+ }
128
+
129
+ /** Iterate present keys in insertion order. O(size). */
130
+ *[Symbol.iterator]() {
131
+ const d = this._dense;
132
+ for (let i = 0; i < this._n; i++) yield d[i];
133
+ }
134
+
135
+ // ---- cold path only: throw builders (string concat lives here, off the hot body) ----
136
+
137
+ /** @private */
138
+ _oob(k) {
139
+ // String(k) -- NOT '+ k' / template literal: those THROW on a Symbol,
140
+ // which would turn a fail-closed reject into a different crash.
141
+ throw new RangeError('[lite-o1] key out of universe [0, ' + this._universe + '): ' + String(k));
142
+ }
143
+
144
+ /** @private */
145
+ _full() {
146
+ throw new RangeError('[lite-o1] SparseSet full (capacity ' + this._cap + ')');
147
+ }
148
+ }
package/README.md ADDED
@@ -0,0 +1,301 @@
1
+ # @zakkster/lite-o1
2
+
3
+ > Zero-GC, O(1) data structures that PROVE their constant. v0.1.0 ships SparseSet: an integer set with O(1) add / has / delete / iterate and an O(1) clear() that zeroes nothing -- plus a throughput-invariance witness that shows the flat cost curve while a native Set decays.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@zakkster/lite-o1.svg?style=for-the-badge&color=latest)](https://www.npmjs.com/package/@zakkster/lite-o1)
6
+ [![sponsor](https://img.shields.io/badge/sponsor-PeshoVurtoleta-ea4aaa.svg?logo=github)](https://github.com/sponsors/PeshoVurtoleta)
7
+ ![Zero-GC](https://img.shields.io/badge/Zero--GC-Engine-00C853?style=for-the-badge&logo=leaf&logoColor=white)
8
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@zakkster/lite-o1?style=for-the-badge)](https://bundlephobia.com/result?p=@zakkster/lite-o1)
9
+ [![npm downloads](https://img.shields.io/npm/dm/@zakkster/lite-o1?style=for-the-badge&color=blue)](https://www.npmjs.com/package/@zakkster/lite-o1)
10
+ ![TypeScript](https://img.shields.io/badge/TypeScript-Types-informational)
11
+ ![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
12
+ [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](./LICENSE)
13
+
14
+ ## The O(1) toolkit the ecosystem was missing
15
+
16
+ Almost no JavaScript data-structure library ships the evidence that its Big-O claim survives contact with a real engine -- megamorphic call sites, GC pauses, cache misses, deopts. `lite-o1` is a curated, tree-shakeable family of the O(1) structures that actually matter, each zero-GC, each written to teach the trick that buys the constant, and each shipped with a harness that DEMONSTRATES the flat cost curve rather than asserting it. The complexity class IS the product.
17
+
18
+ v0.1.0 is the headline member: **SparseSet**, the textbook O(1) integer set (a dense + sparse array pair) whose `clear()` runs in O(1) by resetting a count and zeroing nothing at all.
19
+
20
+ ```bash
21
+ npm install @zakkster/lite-o1
22
+ ```
23
+
24
+ ```js
25
+ import { SparseSet } from '@zakkster/lite-o1';
26
+
27
+ // Universe [0, 100000); at most 10000 entries live at once.
28
+ const live = new SparseSet(100000, 10000);
29
+
30
+ live.add(42);
31
+ live.add(7);
32
+ live.add(42); // idempotent -- still size 2
33
+
34
+ live.has(42); // -> true
35
+ live.has(999); // -> false (absent, never a throw)
36
+ live.has(-1); // -> false (a bad key is absent; null is not zero)
37
+
38
+ live.delete(7); // -> true (swaps the last dense entry into the hole)
39
+ live.size; // -> 1
40
+
41
+ for (const k of live) console.log(k); // 42 (insertion order, alloc-free)
42
+
43
+ live.clear(); // O(1): resets the count, touches NEITHER backing array
44
+ live.size; // -> 0
45
+ ```
46
+
47
+ Every op above is O(1) worst-case and allocates zero bytes after construction. The `witness` harness (`npm run witness`) proves SparseSet holds its ops/ms from n=1e3 to n=1e7 while a native `Set` falls off a cliff.
48
+
49
+ ---
50
+
51
+ ## Table of contents
52
+
53
+ - [Why this exists](#why-this-exists)
54
+ - [What you get](#what-you-get)
55
+ - [How SparseSet works](#how-sparseset-works)
56
+ - [API reference](#api-reference)
57
+ - [SparseSet](#sparseset)
58
+ - [Constants](#constants)
59
+ - [The O(1) Witness](#the-o1-witness)
60
+ - [Composability with the ecosystem](#composability-with-the-ecosystem)
61
+ - [Zero-GC design notes](#zero-gc-design-notes)
62
+ - [Design decisions worth knowing](#design-decisions-worth-knowing)
63
+ - [Testing](#testing)
64
+ - [What this is not](#what-this-is-not)
65
+ - [Ecosystem](#ecosystem)
66
+ - [License](#license)
67
+
68
+ ---
69
+
70
+ ## Why this exists
71
+
72
+ Two problems no small library solves at once for integer sets on a hot path:
73
+
74
+ 1. **The Big-O claim is never proven.** A library says "O(1)" and you take it on faith. But a real engine can turn a nominal O(1) into something that decays with `n`: a hash set's buckets scatter across an ever-larger table until every lookup is a cache miss. `lite-o1`'s analytical anchor is **throughput invariance** -- ops/ms that stays FLAT as `n` grows across orders of magnitude. That flat line IS the proof of the constant, and the shipped witness reports it as a number and a shape, against a built-in `Set` foil on the identical sweep.
75
+
76
+ 2. **The clear() trap.** Emptying a set by zeroing its store is O(n) -- fine once, ruinous in a per-frame loop that refills and clears an ECS component set or a visited-mask every tick. SparseSet's cross-checked membership (`sparse[k] < n && dense[sparse[k]] === k`) makes `clear()` a single `n = 0`: the stale sparse pointers are simply ignored because the cross-check rejects them. Nothing is zeroed, so clearing 10 million entries costs the same as clearing one.
77
+
78
+ Existing options: a native `Set` (arbitrary keys, but a hash table that decays and an O(n) clear), a plain `Array` of flags (O(1) set/test but O(n) clear and O(universe) iterate), or roll-your-own (and get the delete-swap back-pointer wrong). `lite-o1` is the zero-GC primitive for a dense integer domain, with the proof attached.
79
+
80
+ ---
81
+
82
+ ## What you get
83
+
84
+ - **`SparseSet(universe, capacity?)`** -- a zero-GC O(1) integer set over `[0, universe)`, holding at most `capacity` live members (default `capacity = universe`). The hot surface is five ops plus two getters:
85
+ - **`add(k)`** -- insert (idempotent). O(1). Throws a `[lite-o1]` error on a bad key or when full.
86
+ - **`has(k)`** -- membership test. O(1). A bad key (negative, fractional, NaN, null, `>= universe`) is absent -- never a throw.
87
+ - **`delete(k)`** -- remove by swapping the last dense entry into the hole and fixing its back-pointer. O(1). Returns `true` iff present.
88
+ - **`clear()`** -- empty in O(1): resets the live count, zeroes no store.
89
+ - **`forEach(fn)` / `[Symbol.iterator]`** -- iterate present keys in insertion order, alloc-free.
90
+ - **`size` / `capacity`** -- getters.
91
+ - **`VERSION`** -- the package version string.
92
+ - **The O(1) Witness** (`npm run witness`) -- an offline harness that times a fixed batch of the membership op across an n-sweep, reports ops/ms + a flatness ratio for SparseSet against a native `Set` foil, and fails if the constant regressed.
93
+
94
+ Full types ship in [`O1.d.ts`](./O1.d.ts). Tree-shakeable named exports (`sideEffects: false`) -- import only what you use.
95
+
96
+ ---
97
+
98
+ ## How SparseSet works
99
+
100
+ <details>
101
+ <summary>The dense + sparse pair, and why clear() is free.</summary>
102
+
103
+ A SparseSet holds two `Uint32Array`s and a live count `n`:
104
+
105
+ - **`dense`** (capacity-sized) -- `dense[i]` is the i-th member key, packed into `[0, n)` in insertion order. This is what iteration walks.
106
+ - **`sparse`** (universe-sized) -- `sparse[k]` is the index into `dense` where key `k` lives. It is only VALID when the cross-check holds.
107
+
108
+ Membership is a cross-checked double indirection:
109
+
110
+ ```
111
+ has(k) == sparse[k] < n && dense[sparse[k]] === k
112
+ ```
113
+
114
+ That second half is the whole trick. `sparse` is never cleared, so it is full of stale pointers from previous fills. A stale pointer either aims past the live prefix (`sparse[k] >= n`, rejected) or into a slot now holding a different key (`dense[sparse[k]] !== k`, rejected). Either way, a key that is not a member reads as absent -- so:
115
+
116
+ - **`clear()`** is `n = 0`. Every prior key now fails `sparse[k] < n`. No store is touched; clearing 10M entries is O(1).
117
+ - **`add(k)`** appends: `dense[n] = k; sparse[k] = n; n++`. Idempotent because the cross-check catches a re-add.
118
+ - **`delete(k)`** fills the hole with the last live entry so `dense` stays packed: move `dense[n-1]` into `dense[sparse[k]]`, fix that moved key's `sparse` back-pointer, then `n--`. O(1), no shifting.
119
+
120
+ Because `dense` is packed and contiguous, iteration is a linear scan over `[0, n)` -- cache-friendly and alloc-free. Because `sparse` is a flat typed array indexed by the key, lookup is two dependent loads with no hashing and no pointer chase. That layout is why the [witness](#the-o1-witness) stays flat where a hash set decays.
121
+
122
+ The cost of the constant is memory: `sparse` is sized to the whole universe (4 bytes per possible key), whether or not a key is ever added. SparseSet is the right tool when the universe is a known, bounded integer range (entity ids, node indices, small key spaces), not for sparse keys over a huge or unbounded domain.
123
+
124
+ </details>
125
+
126
+ ---
127
+
128
+ ## API reference
129
+
130
+ ### SparseSet
131
+
132
+ ```ts
133
+ new SparseSet(universe: number, capacity?: number)
134
+ ```
135
+
136
+ - **`universe`** -- the exclusive key ceiling; valid keys are integers in `[0, universe)`. An integer in `[1, 2^32]`. Sizes the `sparse` array.
137
+ - **`capacity`** -- the maximum number of live members at once. An integer in `[1, universe]`. Defaults to `universe`. Sizes the `dense` array.
138
+
139
+ The constructor validates both up front and throws a `[lite-o1]`-tagged `RangeError` on a non-integer or out-of-range argument (fail closed). All scratch is allocated here; every method afterward allocates nothing.
140
+
141
+ ```ts
142
+ add(k: number): this // insert (idempotent); throws on a bad key or when full
143
+ has(k: number): boolean // membership; a bad key is absent, never a throw
144
+ delete(k: number): boolean // remove via swap-the-last; true iff k was present
145
+ clear(): void // O(1) empty; zeroes no store
146
+ forEach(fn: (key: number, set: SparseSet) => void): void // insertion order, alloc-free
147
+ [Symbol.iterator](): IterableIterator<number> // insertion order
148
+ get size: number // live member count
149
+ get capacity: number // max live members as constructed
150
+ ```
151
+
152
+ - **`add(k)`** throws `[lite-o1] key out of universe ...` for a key that is not an integer in `[0, universe)` (this includes `-1`, `1.5`, `NaN`, `null`, and `k === universe`), and `[lite-o1] SparseSet full ...` when a NEW key would exceed capacity. Re-adding a present key when full is a no-op, never a throw.
153
+ - **`has(k)` / `delete(k)`** never throw: a bad key is simply absent (`has` returns `false`, `delete` returns `false`). `null` is rejected as `null`, never coerced to key `0` -- `has(null)` is `false` even when `0` is a member.
154
+
155
+ ### Constants
156
+
157
+ | Constant | Value | Meaning |
158
+ | ---------- | --------- | -------------------------------------------------- |
159
+ | `VERSION` | `'0.1.0'` | Package version string. |
160
+
161
+ Contract bounds (validated, not exported):
162
+
163
+ | Bound | Rule |
164
+ | ---------- | ------------------------------------------------ |
165
+ | `universe` | integer in `[1, 2^32]` |
166
+ | `capacity` | integer in `[1, universe]`, default `universe` |
167
+ | valid key | integer in `[0, universe)` |
168
+
169
+ ---
170
+
171
+ ## The O(1) Witness
172
+
173
+ The analytical anchor: **ops/ms that stays flat as n grows is the proof of O(1).** `npm run witness` fills a SparseSet of size `n` and times a fixed batch (1e6) of the membership op at each `n` in a geometric sweep `[1e3, 1e4, 1e5, 1e6, 1e7]`, with a warm-up and the median of 5 reps to reject a loaded-runner stall. It runs a native `Set` foil on the identical key sweep -- the thing a working programmer reaches for by default -- and reports both curves plus a flatness ratio (`opsPerMs(n_max) / opsPerMs(n_min)`):
174
+
175
+ ```
176
+ n SparseSet ops/ms Set ops/ms ratio
177
+ -------- ---------------- ---------- -----
178
+ 1e3 ~552753.40 ~178964.22 ~3.09x
179
+ 1e7 ~443852.64 ~15834.00 ~28.03x
180
+
181
+ SparseSet flatness (last/first): ~0.80 (gate >= 0.70)
182
+ Set foil flatness (last/first): ~0.09 (gate <= 0.55)
183
+ min SparseSet/Set ratio: ~3.09x (gate >= 1.50x)
184
+ ```
185
+
186
+ SparseSet's contiguous typed-array layout streams flat; the `Set`'s hash table scatters across an ever-larger backing store until each lookup is a cache miss, so its ops/ms falls ~11x across the sweep. The gate fails the build if SparseSet flatness drops below `0.70`, the foil fails to decay below `0.55`, or the ratio falls under `1.5x` at any size -- so a regression that quietly ruins the constant fails as loudly as a broken test. (Absolute ops/ms is machine-specific; reproduce on your own hardware.)
187
+
188
+ ---
189
+
190
+ ## Composability with the ecosystem
191
+
192
+ SparseSet is the dense-integer membership primitive under an ECS-style loop. A common pattern: a `SparseSet` per component tracks which entity ids currently have that component; a `@zakkster/lite-arena` `Arena` owns the component payloads by generational handle. Membership and iteration are O(1) and alloc-free; the per-frame `clear()` of a scratch set (visited masks, this-frame-touched ids) is free.
193
+
194
+ ```js
195
+ import { SparseSet } from '@zakkster/lite-o1';
196
+
197
+ const MAX_ENTITIES = 65536;
198
+
199
+ // One membership set per component; iteration is dense and cache-friendly.
200
+ const hasVelocity = new SparseSet(MAX_ENTITIES);
201
+ const hasHealth = new SparseSet(MAX_ENTITIES);
202
+
203
+ // A per-frame scratch set: cleared in O(1) every tick, zero allocation.
204
+ const touchedThisFrame = new SparseSet(MAX_ENTITIES);
205
+
206
+ function spawn(id) { hasVelocity.add(id); hasHealth.add(id); }
207
+
208
+ function tick() {
209
+ touchedThisFrame.clear(); // O(1) -- no store zeroed
210
+ hasVelocity.forEach((id) => {
211
+ // ... integrate motion for `id` (payload from your arena / SoA columns) ...
212
+ touchedThisFrame.add(id);
213
+ });
214
+ // "who moved this frame?" is now an O(1)-membership set, dense-iterable.
215
+ }
216
+
217
+ function despawn(id) {
218
+ hasVelocity.delete(id); // O(1) swap-the-last
219
+ hasHealth.delete(id);
220
+ }
221
+ ```
222
+
223
+ Every stage passes flat `Uint32Array`-backed sets: no boxing, no per-op allocation, no O(n) clear. Pair it with `@zakkster/lite-arena` for generational-handle payload storage (the ECS sibling), or `@zakkster/lite-fastbit32` when a boolean bitmap is enough and iteration order does not matter.
224
+
225
+ ---
226
+
227
+ ## Zero-GC design notes
228
+
229
+ <details>
230
+ <summary>What the hot path allocates (nothing), and how it stays that way.</summary>
231
+
232
+ A SparseSet allocates its two `Uint32Array`s once, at construction. Every method afterward does nothing but integer arithmetic and typed-array reads/writes:
233
+
234
+ | Operation | Steady-state allocations |
235
+ | -------------------------------- | ------------------------ |
236
+ | `add(k)` | **0** |
237
+ | `has(k)` | **0** |
238
+ | `delete(k)` | **0** |
239
+ | `clear()` | **0** (sets `n = 0`) |
240
+ | `forEach(fn)` | **0** |
241
+ | `new SparseSet(...)` | once, at construction (both typed arrays) |
242
+
243
+ The only cold branches are constructor validation and the `_oob` / `_full` throw builders -- the string concatenation that names the offending value lives THERE, off the hot body, so `add` / `has` / `delete` carry no message-formatting bytes. The key check is a single branchless test: `(k >>> 0) !== k` rejects every non-uint32 key (negative, fractional, NaN, null) at once, and `null is not zero` falls out for free (`(null >>> 0) === null` is `false`).
244
+
245
+ The torture gate (`@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`, run under `--expose-gc`) proves it: **0 B/op** on the add/has/delete hot path (per-call allocation measured to the sampling floor), **0 major GCs** and a max pause `<= 2ms` across a 2,000,000-op run, and 100 fill/clear cycles that leave the leak tracker at `size() = 0` (every tracked instance reclaimed -- proven non-vacuously by asserting the tracker held them first) with zero arrayBuffers growth (`clear()` allocates nothing; the reused set grows no backing store). `[Symbol.iterator]` is the one op that allocates -- a single iterator object per `for...of`, not per element -- so a per-frame hot loop uses `forEach`, which is allocation-free.
246
+
247
+ </details>
248
+
249
+ ---
250
+
251
+ ## Design decisions worth knowing
252
+
253
+ - **`clear()` is O(1) because membership is cross-checked, not because the store is wiped.** `has(k)` requires BOTH `sparse[k] < n` AND `dense[sparse[k]] === k`. Resetting `n = 0` invalidates every stale pointer at once. This is the teachable gem; see [`decisions/0001`](./decisions/0001-dense-sparse-crosscheck.md).
254
+ - **The constructor is `(universe, capacity = universe)`.** `universe` (required) sizes the sparse array to the whole key domain; `capacity` (optional) caps live entries and sizes the dense array. Defaulting `capacity` to `universe` gives the simple "a set over `[0, universe)`" case for free while still allowing a tight dense array when you know the live set is small. See [`decisions/0002`](./decisions/0002-hybrid-constructor.md).
255
+ - **Fail closed on add, absent on query.** A bad key to `add` throws (you asked to store something invalid -- a bug). A bad key to `has` / `delete` is simply absent (a query about a non-member is a legitimate `false`). `null` is never coerced to `0`.
256
+ - **Fixed capacity, no silent growth.** A new key past `capacity` throws rather than reallocating. A structure that advertises worst-case O(1) must not hide an amortized O(n) resize; growth, if ever offered, will be opt-in and labeled. See [`decisions/0003`](./decisions/0003-slotpool-deferred.md).
257
+ - **The witness is a first-class deliverable, with a gated floor.** SparseSet flatness `>= 0.70`, the `Set` foil `<= 0.55`, ratio `>= 1.5x` -- a regression in the constant fails the build. See [`decisions/0004`](./decisions/0004-witness-flatness-gate.md).
258
+
259
+ ---
260
+
261
+ ## Testing
262
+
263
+ **19 deterministic `node:test` cases**, plus a torture gate and the O(1) witness gate.
264
+
265
+ ```bash
266
+ npm test # 19 node:test cases (contract + boundary + differential fuzz)
267
+ npm run test:types # tsc --noEmit against O1.d.ts
268
+ npm run torture # @zakkster/lite-leak + lite-gc-profiler: 0 B/op + leak-free
269
+ npm run witness # the O(1) throughput-invariance harness + Set foil + flatness gate
270
+ npm run verify # all four, the publish gate
271
+ ```
272
+
273
+ The suite covers: constructor validation (every bad `universe` / `capacity`), the add/has/delete/clear/iterate surface, the delete-swap back-pointer, idempotent add, insertion-order iteration, the full fail-closed key surface (`add` throws `/^\[lite-o1\]/`, `has` never throws), `null is not zero`, a **byte-identical** proof that `clear()` leaves the dense + sparse `ArrayBuffer`s untouched (snapshot the raw bytes, clear, assert equality, confirm every prior key is absent and a stale pointer cannot masquerade as present), and a **1,000,000-op differential fuzz** of mixed add/delete/has against a native `Set` oracle with zero divergences. No gate output is a FAIL.
274
+
275
+ ---
276
+
277
+ ## What this is not
278
+
279
+ - **Not a general-purpose set.** Keys are integers in a known, bounded `[0, universe)`. For arbitrary keys (strings, objects, huge sparse integer domains), use a native `Set` / `Map` -- SparseSet trades universe-sized memory for the flat constant and the O(1) clear.
280
+ - **Not a growable collection.** Capacity is fixed at construction; a new key past it throws. This is deliberate (worst-case O(1), fail closed), not a missing feature.
281
+ - **Not a payload store.** SparseSet holds membership, not values. Store component data in a parallel SoA column or `@zakkster/lite-arena` keyed by the same ids.
282
+ - **Not the full family yet.** v0.1.0 is SparseSet only. RingDeque, SlotPool, UnionFind, and the eight-dimension benchmark suite are on the roadmap, not in this release.
283
+ - **Not a benchmark suite.** The witness proves throughput invariance (one axis); the full latency/memory/cache/GC benchmark suite is a separate, planned deliverable.
284
+
285
+ ---
286
+
287
+ ## Ecosystem
288
+
289
+ Part of the **@zakkster** zero-GC stack:
290
+
291
+ - [`lite-arena`](https://www.npmjs.com/package/@zakkster/lite-arena) -- zero-GC ECS allocator with generational handles (the payload-storage sibling)
292
+ - [`lite-fastbit32`](https://www.npmjs.com/package/@zakkster/lite-fastbit32) -- branchless 32-bit flag manager (the bitmap primitive)
293
+ - [`lite-lru`](https://www.npmjs.com/package/@zakkster/lite-lru) -- zero-GC cache family under one `LiteCache<K,V>` surface
294
+ - [`lite-leak`](https://www.npmjs.com/package/@zakkster/lite-leak) + [`lite-gc-profiler`](https://www.npmjs.com/package/@zakkster/lite-gc-profiler) -- the torture harness this package is gated by
295
+ - **`lite-o1`** -- this package
296
+
297
+ ---
298
+
299
+ ## License
300
+
301
+ MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
package/llms.txt ADDED
@@ -0,0 +1,80 @@
1
+ # @zakkster/lite-o1
2
+
3
+ Version: 0.1.0
4
+ License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
5
+ Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
6
+ Node: >= 18.
7
+
8
+ ## What it is
9
+
10
+ A tree-shakeable, zero-GC family of O(1) data structures that PROVES its
11
+ constant instead of asserting it. The complexity class is the product: every hot
12
+ op is O(1) worst-case and allocates zero bytes after construction, and a shipped
13
+ witness harness demonstrates the flat throughput curve against a built-in foil.
14
+
15
+ v0.1.0 ships one member: SparseSet.
16
+
17
+ ## Exports (from the single main file O1.js)
18
+
19
+ - `SparseSet` -- class. A zero-GC O(1) integer set over [0, universe).
20
+ - `VERSION` -- string, '0.1.0'.
21
+
22
+ ## SparseSet
23
+
24
+ new SparseSet(universe, capacity = universe)
25
+
26
+ - universe: exclusive key ceiling; integer in [1, 2^32]. Sizes the sparse array.
27
+ - capacity: max live members; integer in [1, universe], default universe. Sizes
28
+ the dense array.
29
+ - Constructor throws a [lite-o1] RangeError on a non-integer / out-of-range arg.
30
+
31
+ Hot surface (all O(1) worst-case, zero allocation):
32
+
33
+ - add(k) -> this Insert; idempotent. Throws [lite-o1] on a bad key
34
+ (not an integer in [0, universe): -1, 1.5, NaN, null,
35
+ k === universe) or when a NEW key exceeds capacity.
36
+ Re-adding a present key when full is a no-op.
37
+ - has(k) -> boolean Membership. A bad key is ABSENT, never a throw. null is
38
+ not zero: has(null) is false even when 0 is a member.
39
+ - delete(k) -> boolean Remove by swapping the last dense entry into the hole and
40
+ fixing its back-pointer. true iff k was present. Never throws.
41
+ - clear() -> void Empty in O(1): resets the live count, zeroes NO store.
42
+ Stale sparse pointers fail the has() cross-check.
43
+ - forEach(fn) -> void Iterate present keys in insertion order, alloc-free.
44
+ fn is (key, set).
45
+ - [Symbol.iterator]() Iterate present keys in insertion order.
46
+ - size (getter) Live member count.
47
+ - capacity (getter) Max live members as constructed.
48
+
49
+ Membership invariant (the whole trick):
50
+
51
+ has(k) == sparse[k] < n && dense[sparse[k]] === k
52
+
53
+ The cross-check rejects stale sparse pointers, which is why clear() can be O(1)
54
+ and never zero a store.
55
+
56
+ ## Design bounds
57
+
58
+ - valid key: integer in [0, universe).
59
+ - Fixed capacity: no silent growth; a new key past capacity throws (fail closed).
60
+ - Fail closed on add, absent on query. null is never coerced to key 0.
61
+
62
+ ## Scripts
63
+
64
+ - npm test -- 19 node:test cases (contract + boundary + differential fuzz).
65
+ - npm run test:types -- tsc --noEmit against O1.d.ts.
66
+ - npm run torture -- lite-leak + lite-gc-profiler: 0 B/op, 0 major GC, leak-free.
67
+ - npm run witness -- O(1) throughput-invariance harness + Set foil + flatness gate.
68
+ - npm run verify -- all four (publish gate).
69
+
70
+ ## Witness gate (locked)
71
+
72
+ - n-sweep [1e3, 1e4, 1e5, 1e6, 1e7], batch 1e6, warm-up + median of 5.
73
+ - SparseSet flatness (opsPerMs last/first) >= 0.70.
74
+ - Native Set foil flatness <= 0.55 AND SparseSet/Set ops-per-ms ratio >= 1.5x.
75
+
76
+ ## Not in v0.1.0
77
+
78
+ RingDeque, SlotPool, UnionFind, MonoDeque, and the eight-dimension benchmark
79
+ suite are on the roadmap, not this release. SparseSet holds membership, not
80
+ payloads (store values in a parallel SoA column or @zakkster/lite-arena).
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@zakkster/lite-o1",
3
+ "author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
4
+ "version": "0.1.0",
5
+ "description": "Zero-dependency, zero-GC family of O(1) data structures that proves its constant: SparseSet (O(1) add/has/delete/clear/iterate) with a throughput-invariance witness harness. Tree-shakeable named exports.",
6
+ "type": "module",
7
+ "main": "./O1.js",
8
+ "module": "./O1.js",
9
+ "types": "./O1.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./O1.d.ts",
13
+ "node": "./O1.js",
14
+ "import": "./O1.js",
15
+ "default": "./O1.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "O1.js",
20
+ "O1.d.ts",
21
+ "llms.txt",
22
+ "README.md",
23
+ "CHANGELOG.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "test": "node --test test/*.test.js",
28
+ "test:types": "tsc -p test/types/tsconfig.json",
29
+ "torture": "node --expose-gc test/torture.mjs",
30
+ "witness": "node test/witness.mjs",
31
+ "verify": "npm test && npm run test:types && npm run torture && npm run witness"
32
+ },
33
+ "keywords": [
34
+ "o1",
35
+ "big-o",
36
+ "sparse-set",
37
+ "data-structures",
38
+ "zero-gc",
39
+ "gc",
40
+ "garbage-collection",
41
+ "typed-array",
42
+ "ecs",
43
+ "performance",
44
+ "lightweight",
45
+ "zero-dependency"
46
+ ],
47
+ "license": "MIT",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "devDependencies": {
52
+ "@zakkster/lite-gc-profiler": "^1.16.0",
53
+ "@zakkster/lite-leak": "^1.10.0",
54
+ "typescript": "^7.0.2"
55
+ },
56
+ "engines": {
57
+ "node": ">=18"
58
+ },
59
+ "sideEffects": false
60
+ }