@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 +37 -0
- package/LICENSE +21 -0
- package/O1.d.ts +51 -0
- package/O1.js +148 -0
- package/README.md +301 -0
- package/llms.txt +80 -0
- package/package.json +60 -0
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
|
+
[](https://www.npmjs.com/package/@zakkster/lite-o1)
|
|
6
|
+
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
7
|
+

|
|
8
|
+
[](https://bundlephobia.com/result?p=@zakkster/lite-o1)
|
|
9
|
+
[](https://www.npmjs.com/package/@zakkster/lite-o1)
|
|
10
|
+

|
|
11
|
+

|
|
12
|
+
[](./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
|
+
}
|