@zakkster/lite-o1 0.1.0 → 0.2.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 +47 -0
- package/O1.d.ts +51 -0
- package/O1.js +198 -3
- package/README.md +137 -18
- package/llms.txt +61 -16
- package/package.json +22 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,53 @@ All notable changes to `@zakkster/lite-o1` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
|
|
5
5
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-09-15
|
|
8
|
+
|
|
9
|
+
The second member of the O(1) family: a fixed-capacity double-ended queue that
|
|
10
|
+
kills the `Array.prototype.shift` O(n) trap. Tree-shakeable alongside SparseSet
|
|
11
|
+
(the two share no mutable module state).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`RingDeque(capacity)`** -- a zero-GC O(1) fixed-capacity double-ended queue
|
|
16
|
+
over ONE `Float64Array` (numeric values only), head + count representation:
|
|
17
|
+
- `pushFront(v)` / `pushBack(v)` / `popFront()` / `popBack()` / `peekFront()` /
|
|
18
|
+
`peekBack()` / `clear()` / `forEach(fn)` / `[Symbol.iterator]` -- all O(1)
|
|
19
|
+
worst-case, zero allocation after construction. `size` and `capacity` getters.
|
|
20
|
+
- Capacity ROUNDS UP to the next power of two (`>= requested`); the ring wraps
|
|
21
|
+
by a single `& (capacity - 1)`. The `capacity` getter reports the rounded
|
|
22
|
+
value. Ceiling: 2^31 elements (a 16 GiB `Float64Array`).
|
|
23
|
+
- `clear()` is O(1): resets head + count and zeroes NO store (numbers retain no
|
|
24
|
+
references, so there is nothing to reclaim).
|
|
25
|
+
- Fail closed: push on a FULL ring throws a `[lite-o1]` error as a
|
|
26
|
+
byte-identical no-op; a non-clean value (non-number or NaN; `+/-Infinity`
|
|
27
|
+
accepted) throws `[lite-o1]` (typeof-guarded before any coercion, so a Symbol
|
|
28
|
+
/ BigInt never triggers a raw `TypeError`). `pop*` / `peek*` on an EMPTY ring
|
|
29
|
+
return `undefined` and never throw.
|
|
30
|
+
- **`O1.d.ts`** -- RingDeque ambient types added.
|
|
31
|
+
- **The O(1) Witness** (`test/witness.mjs`) -- a RingDeque FIFO-churn sweep
|
|
32
|
+
`[1e3, 1e4, 1e5]` vs an `Array.prototype.shift` foil (O(n)); RingDeque flatness
|
|
33
|
+
`>= 0.70`, shift foil `<= 0.55`, ratio `>= 1.5x`.
|
|
34
|
+
- **Torture gate** -- RingDeque fill/drain + both-ends interleave cycles at 0 B/op,
|
|
35
|
+
0 major GC, tracker size 0, arrayBuffers delta 0.
|
|
36
|
+
- **Perf gate** (`test/perf/PerfGate.test.mjs`) -- RingDeque FIFO, LIFO, and
|
|
37
|
+
both-ends interleave scenarios at 0 scavenges / 0 old-gen / 0 arrayBuffers and a
|
|
38
|
+
0-delta grows-counter on the `Float64Array` backing.
|
|
39
|
+
- **RingDeque `node:test` cases** -- contract + boundary + a 1,000,000-op
|
|
40
|
+
differential fuzz at both ends against a plain-`Array` reference deque (0
|
|
41
|
+
divergences; the full-throw and empty-undefined edges both exercised).
|
|
42
|
+
- ADRs [`0005`](./decisions/0005-ring-capacity-fail-closed.md) (fixed power-of-two
|
|
43
|
+
capacity, fail closed on full) and
|
|
44
|
+
[`0006`](./decisions/0006-numeric-ring-substrate.md) (numeric Float64Array
|
|
45
|
+
substrate, undefined-on-empty, clear-untouched).
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- `VERSION` bumped to `'0.2.0'` (synced across `package.json`, the `VERSION` const
|
|
50
|
+
in `O1.js`, and `llms.txt`).
|
|
51
|
+
|
|
52
|
+
[0.2.0]: https://www.npmjs.com/package/@zakkster/lite-o1/v/0.2.0
|
|
53
|
+
|
|
7
54
|
## [0.1.0] - 2026-09-15
|
|
8
55
|
|
|
9
56
|
Initial release. The headline member of the O(1) family, plus the analytical
|
package/O1.d.ts
CHANGED
|
@@ -49,3 +49,54 @@ export class SparseSet {
|
|
|
49
49
|
/** Iterate present keys in insertion order. */
|
|
50
50
|
[Symbol.iterator](): IterableIterator<number>;
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A zero-GC O(1) fixed-capacity double-ended queue over ONE Float64Array
|
|
55
|
+
* (numeric values only). pushFront / pushBack / popFront / popBack / peekFront /
|
|
56
|
+
* peekBack / clear / iterate are all O(1) worst-case and allocate nothing after
|
|
57
|
+
* construction. The requested capacity rounds UP to the next power of two, so the
|
|
58
|
+
* ring wraps by a single `& (capacity - 1)`. Fail closed: push* on a full deque
|
|
59
|
+
* or of a non-clean number (non-number or NaN; +/-Infinity accepted) throws a
|
|
60
|
+
* [lite-o1] error; pop* / peek* on an empty deque return `undefined` and never
|
|
61
|
+
* throw. `clear()` is O(1) and touches no store.
|
|
62
|
+
*/
|
|
63
|
+
export class RingDeque {
|
|
64
|
+
/**
|
|
65
|
+
* @param capacity requested max elements; an integer in [1, 2^31]. Rounded
|
|
66
|
+
* UP to the next power of two.
|
|
67
|
+
*/
|
|
68
|
+
constructor(capacity: number);
|
|
69
|
+
|
|
70
|
+
/** Number of live elements. */
|
|
71
|
+
readonly size: number;
|
|
72
|
+
|
|
73
|
+
/** Max elements this ring holds (power-of-two, rounded up from requested). */
|
|
74
|
+
readonly capacity: number;
|
|
75
|
+
|
|
76
|
+
/** Push v onto the front. Throws a [lite-o1] error when full or on a bad value. */
|
|
77
|
+
pushFront(v: number): this;
|
|
78
|
+
|
|
79
|
+
/** Push v onto the back. Throws a [lite-o1] error when full or on a bad value. */
|
|
80
|
+
pushBack(v: number): this;
|
|
81
|
+
|
|
82
|
+
/** Remove and return the front element, or `undefined` when empty. Never throws. */
|
|
83
|
+
popFront(): number | undefined;
|
|
84
|
+
|
|
85
|
+
/** Remove and return the back element, or `undefined` when empty. Never throws. */
|
|
86
|
+
popBack(): number | undefined;
|
|
87
|
+
|
|
88
|
+
/** Peek the front element, or `undefined` when empty. Never throws. */
|
|
89
|
+
peekFront(): number | undefined;
|
|
90
|
+
|
|
91
|
+
/** Peek the back element, or `undefined` when empty. Never throws. */
|
|
92
|
+
peekBack(): number | undefined;
|
|
93
|
+
|
|
94
|
+
/** Empty the deque in O(1) (resets head + count; zeroes no store). */
|
|
95
|
+
clear(): void;
|
|
96
|
+
|
|
97
|
+
/** Iterate live elements front -> back, alloc-free. */
|
|
98
|
+
forEach(fn: (value: number, index: number, deque: RingDeque) => void): void;
|
|
99
|
+
|
|
100
|
+
/** Iterate live elements front -> back. */
|
|
101
|
+
[Symbol.iterator](): IterableIterator<number>;
|
|
102
|
+
}
|
package/O1.js
CHANGED
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
* that doubles as a teachable textbook: each member solves a real problem AND
|
|
4
4
|
* proves its constant is real (the O(1) Witness -- see test/witness.mjs).
|
|
5
5
|
*
|
|
6
|
-
* v0.
|
|
6
|
+
* v0.2.0 ships two members -- SparseSet and RingDeque -- plus its `VERSION`
|
|
7
|
+
* const. The two are independent (no shared mutable module state), so a bundler
|
|
8
|
+
* that imports one drops the other (`sideEffects: false`).
|
|
7
9
|
*
|
|
8
10
|
* The complexity class IS the product: every hot op below is O(1) worst-case and
|
|
9
11
|
* allocates ZERO bytes after construction. The witness harness (never imported
|
|
10
12
|
* 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.
|
|
13
|
+
* (or Array.prototype.shift) decays -- that flat line is the theorem made visible.
|
|
12
14
|
*
|
|
13
15
|
* @license MIT
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
/** Package version. One of the three version sites (package.json / VERSION / llms.txt). */
|
|
17
|
-
export const VERSION = '0.
|
|
19
|
+
export const VERSION = '0.2.0';
|
|
18
20
|
|
|
19
21
|
/** Largest universe the Uint32 substrate + the (k >>> 0) key check can honor. */
|
|
20
22
|
const MAX_UNIVERSE = 0x100000000; // 2^32
|
|
@@ -146,3 +148,196 @@ export class SparseSet {
|
|
|
146
148
|
throw new RangeError('[lite-o1] SparseSet full (capacity ' + this._cap + ')');
|
|
147
149
|
}
|
|
148
150
|
}
|
|
151
|
+
|
|
152
|
+
/** Largest element count a Float64Array ring can honor (2^31 slots = 16 GiB). */
|
|
153
|
+
const MAX_CAPACITY = 0x80000000; // 2^31
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Round `c` (an integer >= 1) UP to the next power of two >= c. Cold path only
|
|
157
|
+
* (constructor), so a plain doubling loop -- not a bit trick -- is used: `1 << 31`
|
|
158
|
+
* would overflow int32 to a negative, but `p *= 2` walks the double cleanly up to
|
|
159
|
+
* 2^31 in at most 31 steps.
|
|
160
|
+
* @param {number} c
|
|
161
|
+
* @returns {number}
|
|
162
|
+
*/
|
|
163
|
+
function _roundPow2(c) {
|
|
164
|
+
let p = 1;
|
|
165
|
+
while (p < c) p *= 2;
|
|
166
|
+
return p;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* RingDeque -- a zero-GC O(1) fixed-capacity double-ended queue over ONE
|
|
171
|
+
* `Float64Array` (numeric values only).
|
|
172
|
+
*
|
|
173
|
+
* pushFront / pushBack / popFront / popBack / peekFront / peekBack / clear /
|
|
174
|
+
* iterate are ALL O(1) worst-case. The buffer is a CIRCULAR ring: the live window
|
|
175
|
+
* is described by `head` (the index of the front element) and `count` (how many
|
|
176
|
+
* are live). The physical slot for logical offset `i` from the front is:
|
|
177
|
+
*
|
|
178
|
+
* store[(head + i) & MASK]
|
|
179
|
+
*
|
|
180
|
+
* `MASK = capacity - 1`, and `capacity` is a power of two, so the modulo that
|
|
181
|
+
* wraps the index is a single `& MASK` -- no branch, no division. The requested
|
|
182
|
+
* capacity ROUNDS UP to the next power of two (>= requested), and the `capacity`
|
|
183
|
+
* getter reports that rounded value. A pushFront off slot 0 wraps to the top via
|
|
184
|
+
* `(head - 1) & MASK` (int32 `-1 & MASK === MASK`).
|
|
185
|
+
*
|
|
186
|
+
* Fail closed, mirroring SparseSet's discipline exactly:
|
|
187
|
+
* - push* on a FULL deque THROWS a [lite-o1] error, as a byte-identical no-op
|
|
188
|
+
* (store + head + count unchanged -- the throw precedes every write). Cold path.
|
|
189
|
+
* - push* of a value that is not a CLEAN number THROWS [lite-o1]. Rejected:
|
|
190
|
+
* `typeof v !== 'number'` (null, undefined, string, Symbol, BigInt, object)
|
|
191
|
+
* AND NaN (`v !== v`). The typeof guard runs FIRST so a Symbol / BigInt never
|
|
192
|
+
* reaches arithmetic (`+`/`>>>`/template literals THROW a raw TypeError on
|
|
193
|
+
* those -- the recurring cross-package footgun); the cold builder names the
|
|
194
|
+
* value via `String(v)`, which is Symbol/BigInt-safe. +Infinity / -Infinity
|
|
195
|
+
* are CLEAN numbers (typeof number, not NaN) and are ACCEPTED.
|
|
196
|
+
* - pop* / peek* on an EMPTY deque return `undefined`, NEVER throw (mirrors
|
|
197
|
+
* has()'s never-throw query contract). The sentinel is unambiguous because
|
|
198
|
+
* every stored value is a real number, never `undefined`.
|
|
199
|
+
*
|
|
200
|
+
* clear() is O(1) and touches NOTHING: `head = 0; count = 0`. The stale numbers
|
|
201
|
+
* left in the store are unreachable (every read is bounded by `count`), and being
|
|
202
|
+
* numbers they retain no references -- so there is no retention risk and no reason
|
|
203
|
+
* to zero the buffer (mirrors SparseSet.clear()).
|
|
204
|
+
*
|
|
205
|
+
* NOTE: a RingLog / overwrite-oldest preset (pushBack that evicts the front when
|
|
206
|
+
* full instead of throwing) is a DEFERRED future variant -- see decisions/0005.
|
|
207
|
+
*/
|
|
208
|
+
export class RingDeque {
|
|
209
|
+
/**
|
|
210
|
+
* @param {number} capacity requested max elements; an integer in [1, 2^31].
|
|
211
|
+
* Rounded UP to the next power of two.
|
|
212
|
+
*/
|
|
213
|
+
constructor(capacity) {
|
|
214
|
+
// typeof guard BEFORE any coercion: Number.isInteger never coerces (false
|
|
215
|
+
// on a Symbol/BigInt), and String(x) in the cold message is Symbol-safe.
|
|
216
|
+
if (typeof capacity !== 'number' || !Number.isInteger(capacity) ||
|
|
217
|
+
capacity < 1 || capacity > MAX_CAPACITY) {
|
|
218
|
+
throw new RangeError(
|
|
219
|
+
'[lite-o1] RingDeque capacity must be an integer in [1, 2^31], got ' + String(capacity));
|
|
220
|
+
}
|
|
221
|
+
const cap = _roundPow2(capacity);
|
|
222
|
+
this._store = new Float64Array(cap); // the ring buffer (numeric slots)
|
|
223
|
+
this._cap = cap; // power-of-two capacity (rounded)
|
|
224
|
+
this._mask = cap - 1; // wrap mask: (i & MASK) is the physical slot
|
|
225
|
+
this._head = 0; // index of the front element
|
|
226
|
+
this._count = 0; // number of live elements
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Number of live elements. O(1). */
|
|
230
|
+
get size() { return this._count; }
|
|
231
|
+
|
|
232
|
+
/** Max elements this ring can hold (power-of-two, rounded up from requested). O(1). */
|
|
233
|
+
get capacity() { return this._cap; }
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Push v onto the FRONT. O(1). Fails closed: a non-clean value throws via
|
|
237
|
+
* _bad; a full deque throws via _full (byte-identical no-op). Guard typeof
|
|
238
|
+
* FIRST so a Symbol / BigInt never reaches the arithmetic below.
|
|
239
|
+
* @param {number} v a clean number (not NaN; +/-Infinity accepted)
|
|
240
|
+
* @returns {RingDeque} this
|
|
241
|
+
*/
|
|
242
|
+
pushFront(v) {
|
|
243
|
+
if (typeof v !== 'number' || v !== v) return this._bad(v); // v !== v -> NaN
|
|
244
|
+
if (this._count === this._cap) return this._full();
|
|
245
|
+
const h = (this._head - 1) & this._mask; // -1 & MASK wraps to the top slot
|
|
246
|
+
this._store[h] = v;
|
|
247
|
+
this._head = h;
|
|
248
|
+
this._count++;
|
|
249
|
+
return this;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Push v onto the BACK. O(1). Fail-closed identical to pushFront.
|
|
254
|
+
* @param {number} v a clean number (not NaN; +/-Infinity accepted)
|
|
255
|
+
* @returns {RingDeque} this
|
|
256
|
+
*/
|
|
257
|
+
pushBack(v) {
|
|
258
|
+
if (typeof v !== 'number' || v !== v) return this._bad(v); // v !== v -> NaN
|
|
259
|
+
if (this._count === this._cap) return this._full();
|
|
260
|
+
this._store[(this._head + this._count) & this._mask] = v;
|
|
261
|
+
this._count++;
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Remove and return the FRONT element. O(1). Returns `undefined` on an empty
|
|
267
|
+
* deque (never throws) -- unambiguous because every stored value is a number.
|
|
268
|
+
* @returns {number|undefined}
|
|
269
|
+
*/
|
|
270
|
+
popFront() {
|
|
271
|
+
if (this._count === 0) return undefined;
|
|
272
|
+
const v = this._store[this._head];
|
|
273
|
+
this._head = (this._head + 1) & this._mask;
|
|
274
|
+
this._count--;
|
|
275
|
+
return v;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Remove and return the BACK element. O(1). Returns `undefined` on empty.
|
|
280
|
+
* @returns {number|undefined}
|
|
281
|
+
*/
|
|
282
|
+
popBack() {
|
|
283
|
+
if (this._count === 0) return undefined;
|
|
284
|
+
this._count--;
|
|
285
|
+
return this._store[(this._head + this._count) & this._mask];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Peek the FRONT element without removing it. O(1). `undefined` on empty. */
|
|
289
|
+
peekFront() {
|
|
290
|
+
if (this._count === 0) return undefined;
|
|
291
|
+
return this._store[this._head];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Peek the BACK element without removing it. O(1). `undefined` on empty. */
|
|
295
|
+
peekBack() {
|
|
296
|
+
if (this._count === 0) return undefined;
|
|
297
|
+
return this._store[(this._head + this._count - 1) & this._mask];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Empty the deque in O(1). Resets head + count only -- the store is left
|
|
302
|
+
* UNTOUCHED. The stale numbers are unreachable (reads are bounded by count)
|
|
303
|
+
* and retain no references, so there is nothing to zero (mirrors SparseSet).
|
|
304
|
+
*/
|
|
305
|
+
clear() { this._head = 0; this._count = 0; }
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Iterate live elements FRONT -> BACK, alloc-free. O(size). A HOISTED callback
|
|
309
|
+
* makes this a zero-allocation drain.
|
|
310
|
+
* @param {(value:number, index:number, deque:RingDeque)=>void} fn
|
|
311
|
+
*/
|
|
312
|
+
forEach(fn) {
|
|
313
|
+
const store = this._store;
|
|
314
|
+
const mask = this._mask;
|
|
315
|
+
const head = this._head;
|
|
316
|
+
const count = this._count;
|
|
317
|
+
for (let i = 0; i < count; i++) fn(store[(head + i) & mask], i, this);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Iterate live elements FRONT -> BACK. O(size). */
|
|
321
|
+
*[Symbol.iterator]() {
|
|
322
|
+
const store = this._store;
|
|
323
|
+
const mask = this._mask;
|
|
324
|
+
const head = this._head;
|
|
325
|
+
const count = this._count;
|
|
326
|
+
for (let i = 0; i < count; i++) yield store[(head + i) & mask];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---- cold path only: throw builders (string concat lives here, off the hot body) ----
|
|
330
|
+
|
|
331
|
+
/** @private */
|
|
332
|
+
_bad(v) {
|
|
333
|
+
// String(v) -- NOT '+ v' / a template literal: those THROW on a Symbol or
|
|
334
|
+
// BigInt, which would turn a fail-closed reject into a different crash.
|
|
335
|
+
throw new TypeError(
|
|
336
|
+
'[lite-o1] RingDeque value must be a number and not NaN, got ' + String(v));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** @private */
|
|
340
|
+
_full() {
|
|
341
|
+
throw new RangeError('[lite-o1] RingDeque full (capacity ' + this._cap + ')');
|
|
342
|
+
}
|
|
343
|
+
}
|
package/README.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# @zakkster/lite-o1
|
|
2
2
|
|
|
3
|
-
> Zero-GC, O(1) data structures that PROVE their constant. v0.
|
|
3
|
+
> Zero-GC, O(1) data structures that PROVE their constant. v0.2.0 ships SparseSet (an integer set with O(1) add / has / delete / iterate and an O(1) clear() that zeroes nothing) and RingDeque (a fixed-capacity numeric double-ended queue with O(1) push/pop at both ends) -- plus a throughput-invariance witness that shows the flat cost curve while a native Set (or Array.prototype.shift) decays.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@zakkster/lite-o1)
|
|
6
6
|
[](https://github.com/sponsors/PeshoVurtoleta)
|
|
7
7
|

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

|
|
10
12
|

|
|
11
13
|

|
|
12
14
|
[](./LICENSE)
|
|
@@ -15,7 +17,7 @@
|
|
|
15
17
|
|
|
16
18
|
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
19
|
|
|
18
|
-
v0.
|
|
20
|
+
v0.2.0 ships two members. **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. And **RingDeque**, a fixed-capacity double-ended queue of numbers over one circular `Float64Array` -- O(1) push/pop at both ends, the zero-GC answer to the `Array.prototype.shift` O(n) trap. They share no mutable module state, so a bundler that imports one drops the other.
|
|
19
21
|
|
|
20
22
|
```bash
|
|
21
23
|
npm install @zakkster/lite-o1
|
|
@@ -57,6 +59,9 @@ Every op above is O(1) worst-case and allocates zero bytes after construction. T
|
|
|
57
59
|
- [SparseSet](#sparseset)
|
|
58
60
|
- [Constants](#constants)
|
|
59
61
|
- [The O(1) Witness](#the-o1-witness)
|
|
62
|
+
- [RingDeque](#ringdeque)
|
|
63
|
+
- [How RingDeque works](#how-ringdeque-works)
|
|
64
|
+
- [RingDeque API reference](#ringdeque-api-reference)
|
|
60
65
|
- [Composability with the ecosystem](#composability-with-the-ecosystem)
|
|
61
66
|
- [Zero-GC design notes](#zero-gc-design-notes)
|
|
62
67
|
- [Design decisions worth knowing](#design-decisions-worth-knowing)
|
|
@@ -88,8 +93,15 @@ Existing options: a native `Set` (arbitrary keys, but a hash table that decays a
|
|
|
88
93
|
- **`clear()`** -- empty in O(1): resets the live count, zeroes no store.
|
|
89
94
|
- **`forEach(fn)` / `[Symbol.iterator]`** -- iterate present keys in insertion order, alloc-free.
|
|
90
95
|
- **`size` / `capacity`** -- getters.
|
|
96
|
+
- **`RingDeque(capacity)`** -- a zero-GC O(1) fixed-capacity double-ended queue of numbers over one circular `Float64Array`. Capacity rounds up to the next power of two. The hot surface is eight ops plus two getters:
|
|
97
|
+
- **`pushFront(v)` / `pushBack(v)`** -- push at either end. O(1). Throw a `[lite-o1]` error when full (a byte-identical no-op) or on a non-clean value.
|
|
98
|
+
- **`popFront()` / `popBack()`** -- remove + return from either end. O(1). Return `undefined` on empty -- never a throw.
|
|
99
|
+
- **`peekFront()` / `peekBack()`** -- read either end without removing. O(1). `undefined` on empty.
|
|
100
|
+
- **`clear()`** -- empty in O(1): resets head + count, zeroes no store.
|
|
101
|
+
- **`forEach(fn)` / `[Symbol.iterator]`** -- iterate live elements front -> back, alloc-free.
|
|
102
|
+
- **`size` / `capacity`** -- getters (`capacity` reports the rounded power of two).
|
|
91
103
|
- **`VERSION`** -- the package version string.
|
|
92
|
-
- **The O(1) Witness** (`npm run witness`) -- an offline harness that times a fixed batch of
|
|
104
|
+
- **The O(1) Witness** (`npm run witness`) -- an offline harness that times a fixed batch of each member's hot op across an n-sweep, reports ops/ms + a flatness ratio (SparseSet vs a native `Set`, RingDeque vs `Array.prototype.shift`), and fails if the constant regressed.
|
|
93
105
|
|
|
94
106
|
Full types ship in [`O1.d.ts`](./O1.d.ts). Tree-shakeable named exports (`sideEffects: false`) -- import only what you use.
|
|
95
107
|
|
|
@@ -156,15 +168,17 @@ get capacity: number // max live members as constructed
|
|
|
156
168
|
|
|
157
169
|
| Constant | Value | Meaning |
|
|
158
170
|
| ---------- | --------- | -------------------------------------------------- |
|
|
159
|
-
| `VERSION` | `'0.
|
|
171
|
+
| `VERSION` | `'0.2.0'` | Package version string. |
|
|
160
172
|
|
|
161
173
|
Contract bounds (validated, not exported):
|
|
162
174
|
|
|
163
|
-
| Bound
|
|
164
|
-
|
|
|
165
|
-
| `universe
|
|
166
|
-
| `capacity
|
|
167
|
-
| valid key
|
|
175
|
+
| Bound | Rule |
|
|
176
|
+
| ------------------- | ------------------------------------------------ |
|
|
177
|
+
| SparseSet `universe`| integer in `[1, 2^32]` |
|
|
178
|
+
| SparseSet `capacity`| integer in `[1, universe]`, default `universe` |
|
|
179
|
+
| SparseSet valid key | integer in `[0, universe)` |
|
|
180
|
+
| RingDeque `capacity`| integer in `[1, 2^31]`, rounded up to a power of two |
|
|
181
|
+
| RingDeque value | `typeof 'number'` and not `NaN` (`+/-Infinity` OK) |
|
|
168
182
|
|
|
169
183
|
---
|
|
170
184
|
|
|
@@ -187,6 +201,95 @@ SparseSet's contiguous typed-array layout streams flat; the `Set`'s hash table s
|
|
|
187
201
|
|
|
188
202
|
---
|
|
189
203
|
|
|
204
|
+
## RingDeque
|
|
205
|
+
|
|
206
|
+
The second member: a **fixed-capacity double-ended queue of numbers** over one circular `Float64Array`. Push and pop at BOTH ends are O(1) worst-case and allocate zero bytes -- the zero-GC answer to the `Array.prototype.shift` / `unshift` O(n) trap, where every element re-indexes on each end operation.
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
import { RingDeque } from '@zakkster/lite-o1';
|
|
210
|
+
|
|
211
|
+
// Requested 1000 -> capacity rounds UP to the next power of two (1024).
|
|
212
|
+
const q = new RingDeque(1000);
|
|
213
|
+
q.capacity; // -> 1024
|
|
214
|
+
|
|
215
|
+
q.pushBack(1);
|
|
216
|
+
q.pushBack(2);
|
|
217
|
+
q.pushFront(0); // [0, 1, 2]
|
|
218
|
+
|
|
219
|
+
q.peekFront(); // -> 0
|
|
220
|
+
q.peekBack(); // -> 2
|
|
221
|
+
|
|
222
|
+
q.popFront(); // -> 0 (FIFO with pushBack)
|
|
223
|
+
q.popBack(); // -> 2 (LIFO with pushBack)
|
|
224
|
+
q.size; // -> 1
|
|
225
|
+
|
|
226
|
+
for (const v of q) console.log(v); // 1 (front -> back, alloc-free)
|
|
227
|
+
|
|
228
|
+
q.pushBack(Infinity); // OK: +/-Infinity are clean numbers
|
|
229
|
+
// q.pushBack(NaN); // throws [lite-o1]: NaN is rejected
|
|
230
|
+
// q.pushBack('3'); // throws [lite-o1]: not a number
|
|
231
|
+
|
|
232
|
+
q.clear(); // O(1): resets head + count, touches NO store
|
|
233
|
+
q.popFront(); // -> undefined (empty never throws)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Every op is O(1) worst-case and zero-allocation after construction. `pop*` / `peek*` on an empty ring return `undefined` (never throw); the sentinel is unambiguous because every stored value is a real number. A push on a full ring throws a `[lite-o1]` error as a byte-identical no-op -- fail closed, no silent drop or overwrite. The `witness` harness proves RingDeque's FIFO churn holds its ops/ms while `Array.prototype.shift` collapses as `n` grows.
|
|
237
|
+
|
|
238
|
+
### How RingDeque works
|
|
239
|
+
|
|
240
|
+
<details>
|
|
241
|
+
<summary>The circular buffer, head + count, and why clear() is free.</summary>
|
|
242
|
+
|
|
243
|
+
A RingDeque holds one `Float64Array` (the ring), a `head` (the index of the front element), and a `count` (how many elements are live). The physical slot for logical offset `i` from the front is:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
store[(head + i) & MASK] MASK = capacity - 1
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Because `capacity` is a power of two, the modulo that wraps the index is a single bitwise `& MASK` -- no branch, no division. The requested capacity rounds UP to the next power of two (so `new RingDeque(1000)` gives capacity 1024), and the `capacity` getter reports that rounded value.
|
|
250
|
+
|
|
251
|
+
- **`pushBack(v)`** writes `store[(head + count) & MASK] = v; count++`.
|
|
252
|
+
- **`pushFront(v)`** moves the head back one slot (`head = (head - 1) & MASK`, where int32 `-1 & MASK === MASK` wraps off slot 0 to the top), writes `store[head] = v`, then `count++`.
|
|
253
|
+
- **`popFront()`** reads `store[head]`, advances `head = (head + 1) & MASK`, `count--`.
|
|
254
|
+
- **`popBack()`** does `count--` and reads `store[(head + count) & MASK]`.
|
|
255
|
+
|
|
256
|
+
Using **head + count** (not a head/tail pair) makes "full" a single test (`count === capacity`) and "empty" a single test (`count === 0`), with no ambiguous `head === tail` state to disambiguate.
|
|
257
|
+
|
|
258
|
+
- **`clear()`** is `head = 0; count = 0`. The store is left byte-identical. The stale numbers are unreachable (every read is bounded by `count`) and retain no references (they are numbers), so there is nothing to zero -- clearing a full ring costs the same as clearing an empty one. This is the same teachable gem as SparseSet's cross-checked clear.
|
|
259
|
+
|
|
260
|
+
The cost of the constant is the value domain: a `Float64Array` holds numbers only. To queue objects, queue their integer handles / indices and keep the payloads in a parallel column or `@zakkster/lite-arena`.
|
|
261
|
+
|
|
262
|
+
</details>
|
|
263
|
+
|
|
264
|
+
### RingDeque API reference
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
new RingDeque(capacity: number) // capacity rounds up to the next power of two
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
- **`capacity`** -- the requested maximum number of live elements; an integer in `[1, 2^31]`. Rounded UP to the next power of two (`>= requested`); the `capacity` getter reports the rounded value. The constructor throws a `[lite-o1]`-tagged `RangeError` on a non-integer, out-of-range, or non-number argument (typeof-guarded before any coercion, so a Symbol / BigInt fails closed rather than crashing raw).
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
pushFront(v: number): this // push at the front; throws when full / on a bad value
|
|
274
|
+
pushBack(v: number): this // push at the back; throws when full / on a bad value
|
|
275
|
+
popFront(): number | undefined // remove + return the front; undefined on empty
|
|
276
|
+
popBack(): number | undefined // remove + return the back; undefined on empty
|
|
277
|
+
peekFront(): number | undefined // read the front; undefined on empty
|
|
278
|
+
peekBack(): number | undefined // read the back; undefined on empty
|
|
279
|
+
clear(): void // O(1) empty; zeroes no store
|
|
280
|
+
forEach(fn: (value: number, index: number, deque: RingDeque) => void): void // front -> back
|
|
281
|
+
[Symbol.iterator](): IterableIterator<number> // front -> back
|
|
282
|
+
get size: number // live element count
|
|
283
|
+
get capacity: number // max elements (power-of-two, rounded up)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- **`pushFront(v)` / `pushBack(v)`** throw `[lite-o1] RingDeque full ...` when the ring is at capacity (a byte-identical no-op -- store + head + count unchanged), and `[lite-o1] RingDeque value must be a number ...` on a value that is not a clean number. A value is clean iff `typeof v === 'number'` AND it is not `NaN`; `+Infinity` / `-Infinity` are accepted, while `null`, `undefined`, strings, Symbols, BigInts, objects, and `NaN` are rejected. The `typeof` guard runs first so a Symbol / BigInt never reaches arithmetic.
|
|
287
|
+
- **`popFront()` / `popBack()` / `peekFront()` / `peekBack()`** never throw: an empty ring returns `undefined`. Because every stored value is a real number, `undefined` unambiguously means "empty".
|
|
288
|
+
|
|
289
|
+
**Reach for RingDeque when** you need FIFO / LIFO / sliding-window push-pop at O(1) with zero per-op allocation over a bounded numeric domain (ring buffers, bounded work queues, rolling windows). **Avoid it when** you need to queue non-numbers (queue their handles instead), or need the queue to grow past a bound you cannot set up front (it fails closed on a full push rather than resizing). See [`GUIDE.md`](./GUIDE.md) for the full reach-for / avoid / measure-it.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
190
293
|
## Composability with the ecosystem
|
|
191
294
|
|
|
192
295
|
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.
|
|
@@ -244,6 +347,19 @@ The only cold branches are constructor validation and the `_oob` / `_full` throw
|
|
|
244
347
|
|
|
245
348
|
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
349
|
|
|
350
|
+
**RingDeque** allocates its single `Float64Array` once, at construction:
|
|
351
|
+
|
|
352
|
+
| Operation | Steady-state allocations |
|
|
353
|
+
| -------------------------------- | ------------------------ |
|
|
354
|
+
| `pushFront(v)` / `pushBack(v)` | **0** |
|
|
355
|
+
| `popFront()` / `popBack()` | **0** |
|
|
356
|
+
| `peekFront()` / `peekBack()` | **0** |
|
|
357
|
+
| `clear()` | **0** (head + count = 0) |
|
|
358
|
+
| `forEach(fn)` | **0** |
|
|
359
|
+
| `new RingDeque(...)` | once, at construction (one typed array) |
|
|
360
|
+
|
|
361
|
+
The value guard is a two-test branchless check on the hot body -- `typeof v !== 'number' || v !== v` (the second catches NaN once the type is known) -- with the message-building `_bad` / `_full` throw builders on the cold path (again using `String(v)`, never a template literal, so a Symbol / BigInt value fails closed rather than crashing raw). The torture and perf gates prove RingDeque at **0 B/op** across FIFO / LIFO / both-ends interleave churn, with a 0-delta on the `Float64Array` backing (fixed capacity -- no resize) and the leak tracker back at `size() = 0`.
|
|
362
|
+
|
|
247
363
|
</details>
|
|
248
364
|
|
|
249
365
|
---
|
|
@@ -255,31 +371,34 @@ The torture gate (`@zakkster/lite-leak` + `@zakkster/lite-gc-profiler`, run unde
|
|
|
255
371
|
- **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
372
|
- **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
373
|
- **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).
|
|
374
|
+
- **RingDeque is fixed-capacity (power-of-two), fail closed on full, and stores numbers only.** A power-of-two capacity buys the single-`& MASK` wrap; head + count makes full / empty single tests; a full push throws (no silent drop / overwrite); the numeric substrate keeps it zero-GC and makes `undefined`-on-empty unambiguous. See [`decisions/0005`](./decisions/0005-ring-capacity-fail-closed.md) and [`decisions/0006`](./decisions/0006-numeric-ring-substrate.md).
|
|
258
375
|
|
|
259
376
|
---
|
|
260
377
|
|
|
261
378
|
## Testing
|
|
262
379
|
|
|
263
|
-
**
|
|
380
|
+
**58 deterministic `node:test` cases**, plus a torture gate, a hard perf gate, and the O(1) witness gate.
|
|
264
381
|
|
|
265
382
|
```bash
|
|
266
|
-
npm test #
|
|
383
|
+
npm test # 58 node:test cases (contract + boundary + differential fuzz)
|
|
267
384
|
npm run test:types # tsc --noEmit against O1.d.ts
|
|
268
385
|
npm run torture # @zakkster/lite-leak + lite-gc-profiler: 0 B/op + leak-free
|
|
269
|
-
npm run witness # the O(1) throughput-invariance harness +
|
|
270
|
-
npm run
|
|
386
|
+
npm run witness # the O(1) throughput-invariance harness + foils + flatness gate
|
|
387
|
+
npm run test:perf # @zakkster/lite-perf-gate: hard zero-alloc scavenge-scaling gate
|
|
388
|
+
npm run verify # all five, the publish gate
|
|
271
389
|
```
|
|
272
390
|
|
|
273
|
-
|
|
391
|
+
For SparseSet 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, and a **1,000,000-op differential fuzz** of mixed add/delete/has against a native `Set` oracle. For RingDeque: power-of-two capacity rounding, push/pop/peek at both ends, wrap-around across the `& MASK` seam, the fail-closed surface (full push throws as a byte-identical no-op; a non-number or NaN throws; a Symbol / BigInt fails closed, not raw; `+/-Infinity` accepted; empty pop/peek returns `undefined`), a byte-identical `clear()` proof, and a **1,000,000-op both-ends differential fuzz** against a plain-`Array` reference deque (0 divergences, with the full-throw and empty-undefined edges both exercised). No gate output is a FAIL.
|
|
274
392
|
|
|
275
393
|
---
|
|
276
394
|
|
|
277
395
|
## What this is not
|
|
278
396
|
|
|
279
|
-
- **Not a general-purpose set.**
|
|
280
|
-
- **Not a
|
|
281
|
-
- **Not a
|
|
282
|
-
- **Not
|
|
397
|
+
- **Not a general-purpose set.** SparseSet 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.
|
|
398
|
+
- **Not a general-purpose queue.** RingDeque stores numbers only. To queue objects / strings, queue their integer handles and keep the payloads in a parallel column or `@zakkster/lite-arena`.
|
|
399
|
+
- **Not a growable collection.** Both members are fixed-capacity: a SparseSet key past capacity, or a RingDeque push on a full ring, throws. This is deliberate (worst-case O(1), fail closed -- no hidden amortized resize), not a missing feature. An overwrite-oldest RingDeque preset (RingLog) is a deferred future variant, not the current default.
|
|
400
|
+
- **Not a payload store.** SparseSet holds membership, RingDeque holds numbers -- neither holds object payloads. Store component data in a parallel SoA column or `@zakkster/lite-arena` keyed by the same ids / handles.
|
|
401
|
+
- **Not the full family yet.** v0.2.0 is SparseSet + RingDeque. SlotPool, UnionFind, MonoDeque, and the eight-dimension benchmark suite are on the roadmap, not in this release.
|
|
283
402
|
- **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
403
|
|
|
285
404
|
---
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @zakkster/lite-o1
|
|
2
2
|
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
License: MIT (c) Zahary Shinikchiev <shinikchiev@yahoo.com>
|
|
5
5
|
Runtime dependencies: none. ESM only. ASCII-only source. sideEffects: false.
|
|
6
6
|
Node: >= 18.
|
|
@@ -12,12 +12,14 @@ constant instead of asserting it. The complexity class is the product: every hot
|
|
|
12
12
|
op is O(1) worst-case and allocates zero bytes after construction, and a shipped
|
|
13
13
|
witness harness demonstrates the flat throughput curve against a built-in foil.
|
|
14
14
|
|
|
15
|
-
v0.
|
|
15
|
+
v0.2.0 ships two members: SparseSet and RingDeque. They share no mutable module
|
|
16
|
+
state, so a bundler that imports one drops the other (tree-shakeable).
|
|
16
17
|
|
|
17
18
|
## Exports (from the single main file O1.js)
|
|
18
19
|
|
|
19
20
|
- `SparseSet` -- class. A zero-GC O(1) integer set over [0, universe).
|
|
20
|
-
- `
|
|
21
|
+
- `RingDeque` -- class. A zero-GC O(1) fixed-capacity numeric double-ended queue.
|
|
22
|
+
- `VERSION` -- string, '0.2.0'.
|
|
21
23
|
|
|
22
24
|
## SparseSet
|
|
23
25
|
|
|
@@ -53,28 +55,71 @@ Membership invariant (the whole trick):
|
|
|
53
55
|
The cross-check rejects stale sparse pointers, which is why clear() can be O(1)
|
|
54
56
|
and never zero a store.
|
|
55
57
|
|
|
58
|
+
## RingDeque
|
|
59
|
+
|
|
60
|
+
new RingDeque(capacity)
|
|
61
|
+
|
|
62
|
+
- capacity: requested max elements; integer in [1, 2^31]. ROUNDS UP to the next
|
|
63
|
+
power of two (>= requested); the capacity getter reports the rounded value.
|
|
64
|
+
- Constructor throws a [lite-o1] RangeError on a non-integer / out-of-range /
|
|
65
|
+
non-number arg (typeof-guarded before any coercion; message via String(x)).
|
|
66
|
+
- Substrate: ONE Float64Array (numeric values only), head + count representation.
|
|
67
|
+
The physical slot for logical offset i from the front is store[(head + i) & MASK],
|
|
68
|
+
MASK = capacity - 1; wrap is a single & (power-of-two modulo), no branch.
|
|
69
|
+
|
|
70
|
+
Hot surface (all O(1) worst-case, zero allocation):
|
|
71
|
+
|
|
72
|
+
- pushFront(v) -> this Push v onto the front. Throws [lite-o1] when FULL (a
|
|
73
|
+
byte-identical no-op) or on a non-clean value.
|
|
74
|
+
- pushBack(v) -> this Push v onto the back. Same fail-closed policy.
|
|
75
|
+
- popFront() -> number|undefined Remove+return the front; undefined on empty (never throws).
|
|
76
|
+
- popBack() -> number|undefined Remove+return the back; undefined on empty (never throws).
|
|
77
|
+
- peekFront() -> number|undefined Front without removing; undefined on empty.
|
|
78
|
+
- peekBack() -> number|undefined Back without removing; undefined on empty.
|
|
79
|
+
- clear() -> void Empty in O(1): resets head + count, touches NO store
|
|
80
|
+
(numbers retain no references, so nothing to zero).
|
|
81
|
+
- forEach(fn) -> void Iterate live elements front->back, alloc-free.
|
|
82
|
+
fn is (value, index, deque).
|
|
83
|
+
- [Symbol.iterator]() Iterate live elements front->back.
|
|
84
|
+
- size (getter) Live element count.
|
|
85
|
+
- capacity (getter) Max elements (power-of-two, rounded up from requested).
|
|
86
|
+
|
|
87
|
+
Value contract (LOCKED): a pushed value must be typeof 'number' AND not NaN.
|
|
88
|
+
Rejected (throws [lite-o1]): null, undefined, string, Symbol, BigInt, object, NaN.
|
|
89
|
+
Accepted: any finite number, and +Infinity / -Infinity (typeof number, not NaN).
|
|
90
|
+
The typeof guard runs FIRST so a Symbol / BigInt never reaches arithmetic.
|
|
91
|
+
|
|
56
92
|
## Design bounds
|
|
57
93
|
|
|
58
|
-
- valid key: integer in [0, universe).
|
|
59
|
-
-
|
|
60
|
-
-
|
|
94
|
+
- SparseSet valid key: integer in [0, universe).
|
|
95
|
+
- SparseSet fixed capacity: no silent growth; a new key past capacity throws.
|
|
96
|
+
- SparseSet fail closed on add, absent on query. null is never coerced to key 0.
|
|
97
|
+
- RingDeque fixed capacity (power-of-two, rounded up): push past full throws
|
|
98
|
+
(fail closed); pop/peek on empty returns undefined. Numeric values only.
|
|
99
|
+
- RingDeque value: typeof 'number' and not NaN (+/-Infinity accepted).
|
|
61
100
|
|
|
62
101
|
## Scripts
|
|
63
102
|
|
|
64
|
-
- npm test --
|
|
103
|
+
- npm test -- 58 node:test cases (contract + boundary + differential fuzz).
|
|
65
104
|
- npm run test:types -- tsc --noEmit against O1.d.ts.
|
|
66
105
|
- 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 +
|
|
68
|
-
- npm run
|
|
106
|
+
- npm run witness -- O(1) throughput-invariance harness + foils + flatness gate.
|
|
107
|
+
- npm run test:perf -- lite-perf-gate hard zero-alloc scavenge-scaling gate.
|
|
108
|
+
- npm run verify -- all five (publish gate).
|
|
69
109
|
|
|
70
110
|
## Witness gate (locked)
|
|
71
111
|
|
|
72
|
-
- n-sweep [1e3
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
- SparseSet: n-sweep [1e3..1e7], batch 1e6, warm-up + median of 5. SparseSet
|
|
113
|
+
flatness (opsPerMs last/first) >= 0.70; native Set foil flatness <= 0.55 AND
|
|
114
|
+
SparseSet/Set ops-per-ms ratio >= 1.5x.
|
|
115
|
+
- RingDeque: n-sweep [1e3, 1e4, 1e5], FIFO churn vs Array.prototype.shift foil
|
|
116
|
+
(O(n)); ops/ms is a rate, so the ring (batch 5e5) and foil (batch 2e3) use
|
|
117
|
+
different batches yet compare directly. RingDeque flatness >= 0.70; shift foil
|
|
118
|
+
flatness <= 0.55; RingDeque/shift ratio >= 1.5x.
|
|
75
119
|
|
|
76
|
-
## Not
|
|
120
|
+
## Not yet shipped
|
|
77
121
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
122
|
+
SlotPool, UnionFind, MonoDeque, and the eight-dimension benchmark suite are on
|
|
123
|
+
the roadmap, not this release. A RingLog / overwrite-oldest RingDeque preset is a
|
|
124
|
+
deferred future variant. SparseSet holds membership, not payloads (store values
|
|
125
|
+
in a parallel SoA column or @zakkster/lite-arena); RingDeque holds numbers only.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zakkster/lite-o1",
|
|
3
3
|
"author": "Zahary Shinikchiev <shinikchiev@yahoo.com>",
|
|
4
|
-
"version": "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.",
|
|
4
|
+
"version": "0.2.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) and RingDeque (O(1) fixed-capacity numeric double-ended queue) with a throughput-invariance witness harness. Tree-shakeable named exports.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./O1.js",
|
|
8
8
|
"module": "./O1.js",
|
|
@@ -28,12 +28,17 @@
|
|
|
28
28
|
"test:types": "tsc -p test/types/tsconfig.json",
|
|
29
29
|
"torture": "node --expose-gc test/torture.mjs",
|
|
30
30
|
"witness": "node test/witness.mjs",
|
|
31
|
-
"
|
|
31
|
+
"test:perf": "node --expose-gc --max-semi-space-size=4 --test test/perf/PerfGate.test.mjs",
|
|
32
|
+
"verify": "npm test && npm run test:types && npm run torture && npm run witness && npm run test:perf"
|
|
32
33
|
},
|
|
33
34
|
"keywords": [
|
|
34
35
|
"o1",
|
|
35
36
|
"big-o",
|
|
36
37
|
"sparse-set",
|
|
38
|
+
"ring-buffer",
|
|
39
|
+
"circular-buffer",
|
|
40
|
+
"deque",
|
|
41
|
+
"ringdeque",
|
|
37
42
|
"data-structures",
|
|
38
43
|
"zero-gc",
|
|
39
44
|
"gc",
|
|
@@ -51,10 +56,24 @@
|
|
|
51
56
|
"devDependencies": {
|
|
52
57
|
"@zakkster/lite-gc-profiler": "^1.16.0",
|
|
53
58
|
"@zakkster/lite-leak": "^1.10.0",
|
|
59
|
+
"@zakkster/lite-perf-gate": "^1.4.2",
|
|
54
60
|
"typescript": "^7.0.2"
|
|
55
61
|
},
|
|
62
|
+
"homepage": "https://github.com/PeshoVurtoleta/lite-o1#readme",
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/PeshoVurtoleta/lite-o1.git"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/PeshoVurtoleta/lite-o1/issues",
|
|
69
|
+
"email": "shinikchiev@yahoo.com"
|
|
70
|
+
},
|
|
56
71
|
"engines": {
|
|
57
72
|
"node": ">=18"
|
|
58
73
|
},
|
|
74
|
+
"funding": {
|
|
75
|
+
"type": "github",
|
|
76
|
+
"url": "https://github.com/sponsors/PeshoVurtoleta"
|
|
77
|
+
},
|
|
59
78
|
"sideEffects": false
|
|
60
79
|
}
|