@nxtedition/slice
Advanced tools
+1
-1
@@ -7,3 +7,3 @@ import util from 'node:util'; | ||
| }; | ||
| export declare class Slice { | ||
| export declare class Slice implements SliceLike { | ||
| buffer: Buffer; | ||
@@ -10,0 +10,0 @@ byteOffset: number; |
+64
-10
@@ -11,3 +11,3 @@ import util from 'node:util' | ||
| export class Slice { | ||
| export class Slice { | ||
| buffer = EMPTY_BUF | ||
@@ -78,2 +78,8 @@ byteOffset = 0 | ||
| } else { | ||
| // Start offsets are validated (not clamped) so a negative offset can | ||
| // never resolve to a position before the slice and read adjacent | ||
| // (pool) memory. | ||
| if (sourceStart < 0 || sourceStart > this.byteLength || !Number.isInteger(sourceStart)) { | ||
| throw new RangeError(`Invalid sourceStart: ${sourceStart}`) | ||
| } | ||
| sourceStart += this.byteOffset | ||
@@ -88,7 +94,10 @@ } | ||
| // Clamp against the logical slice length so copy() cannot read past | ||
| // the slice's own end and leak adjacent (pool) memory. | ||
| // Clamp the end against the logical slice length so copy() cannot read | ||
| // past the slice's own end and leak adjacent (pool) memory. | ||
| if (sourceEnd > sliceEnd) { | ||
| sourceEnd = sliceEnd | ||
| } | ||
| if (sourceEnd < sourceStart) { | ||
| sourceEnd = sourceStart | ||
| } | ||
@@ -100,2 +109,5 @@ if (target instanceof Slice) { | ||
| } else { | ||
| if (targetStart < 0 || targetStart > target.byteLength || !Number.isInteger(targetStart)) { | ||
| throw new RangeError(`Invalid targetStart: ${targetStart}`) | ||
| } | ||
| targetStart += target.byteOffset | ||
@@ -145,2 +157,5 @@ } | ||
| } else { | ||
| if (targetStart < 0 || targetStart > target.byteLength || !Number.isInteger(targetStart)) { | ||
| throw new RangeError(`Invalid targetStart: ${targetStart}`) | ||
| } | ||
| targetStart += target.byteOffset | ||
@@ -158,2 +173,5 @@ } | ||
| } | ||
| if (targetEnd < targetStart) { | ||
| targetEnd = targetStart | ||
| } | ||
| target = target.buffer | ||
@@ -165,2 +183,5 @@ } | ||
| } else { | ||
| if (sourceStart < 0 || sourceStart > this.byteLength || !Number.isInteger(sourceStart)) { | ||
| throw new RangeError(`Invalid sourceStart: ${sourceStart}`) | ||
| } | ||
| sourceStart += this.byteOffset | ||
@@ -178,2 +199,5 @@ } | ||
| } | ||
| if (sourceEnd < sourceStart) { | ||
| sourceEnd = sourceStart | ||
| } | ||
@@ -189,3 +213,3 @@ return this.buffer.compare(target, targetStart, targetEnd, sourceStart, sourceEnd) | ||
| } else { | ||
| if (offset < 0 || offset > this.byteLength) { | ||
| if (offset < 0 || offset > this.byteLength || !Number.isInteger(offset)) { | ||
| throw new RangeError(`Invalid offset: ${offset}`) | ||
@@ -199,4 +223,9 @@ } | ||
| length = available | ||
| } else if (length > available) { | ||
| length = available | ||
| } else { | ||
| if (length < 0 || !Number.isInteger(length)) { | ||
| throw new RangeError(`Invalid length: ${length}`) | ||
| } | ||
| if (length > available) { | ||
| length = available | ||
| } | ||
| } | ||
@@ -215,2 +244,7 @@ | ||
| } else { | ||
| // Validate (not clamp): a negative offset would resolve to a position | ||
| // before the slice and corrupt adjacent (pool) memory. | ||
| if (offset < 0 || offset > this.byteLength || !Number.isInteger(offset)) { | ||
| throw new RangeError(`Invalid offset: ${offset}`) | ||
| } | ||
| offset += this.byteOffset | ||
@@ -229,3 +263,3 @@ } | ||
| at(index ) { | ||
| if (index >= this.byteLength || index < -this.byteLength) { | ||
| if (!Number.isInteger(index) || index >= this.byteLength || index < -this.byteLength) { | ||
| throw new RangeError(`Index out of range: ${index}`) | ||
@@ -250,2 +284,5 @@ } | ||
| } else { | ||
| if (start < 0 || start > this.byteLength || !Number.isInteger(start)) { | ||
| throw new RangeError(`Invalid start: ${start}`) | ||
| } | ||
| start += this.byteOffset | ||
@@ -263,2 +300,5 @@ } | ||
| } | ||
| if (end < start) { | ||
| end = start | ||
| } | ||
@@ -274,2 +314,5 @@ return this.buffer.toString(encoding, start, end) | ||
| } else { | ||
| if (start < 0 || start > this.byteLength || !Number.isInteger(start)) { | ||
| throw new RangeError(`Invalid start: ${start}`) | ||
| } | ||
| start += this.byteOffset | ||
@@ -287,2 +330,5 @@ } | ||
| } | ||
| if (end < start) { | ||
| end = start | ||
| } | ||
@@ -301,4 +347,10 @@ return start === 0 && end === this.buffer.byteLength | ||
| const len = this.byteLength | ||
| const shown = len < MAX_BYTES ? len : MAX_BYTES | ||
| // Never read past the underlying buffer. A malformed slice (byteOffset / | ||
| // byteLength extending beyond the buffer) is exactly the state a developer | ||
| // inspects while debugging pool corruption — inspect must not throw on it. | ||
| const available = this.buffer.byteLength - this.byteOffset | ||
| const safeLen = available > 0 ? (len < available ? len : available) : 0 | ||
| const shown = safeLen < MAX_BYTES ? safeLen : MAX_BYTES | ||
| let hex = '' | ||
@@ -315,4 +367,3 @@ for (let i = 0; i < shown; i++) { | ||
| const strEnd = shown < len ? this.byteOffset + shown : this.byteOffset + len | ||
| const str = this.buffer.toString('utf8', this.byteOffset, strEnd) | ||
| const str = this.buffer.toString('utf8', this.byteOffset, this.byteOffset + shown) | ||
| const truncated = shown < len ? '…' : '' | ||
@@ -341,2 +392,5 @@ | ||
| if (typeof poolTotalOrBuffer === 'number') { | ||
| if (!Number.isInteger(poolTotalOrBuffer) || poolTotalOrBuffer < 0) { | ||
| throw new RangeError(`Invalid pool size: ${poolTotalOrBuffer}`) | ||
| } | ||
| this.#poolBuffer = Buffer.allocUnsafeSlow(poolTotalOrBuffer) | ||
@@ -343,0 +397,0 @@ } else if (poolTotalOrBuffer instanceof Buffer) { |
+2
-2
| { | ||
| "name": "@nxtedition/slice", | ||
| "version": "1.1.9", | ||
| "version": "1.1.10", | ||
| "type": "module", | ||
@@ -33,3 +33,3 @@ "main": "lib/index.js", | ||
| }, | ||
| "gitHead": "2c131da7ca8ea328fd681e975dd7caf57f9f4896" | ||
| "gitHead": "7c9c7457c885c644c7a1e70ef894d4727ce240d6" | ||
| } |
+33
-11
@@ -13,3 +13,3 @@ # @nxtedition/slice | ||
| `PoolAllocator` takes this further. Like Node's internal pool, it has management overhead — but it rarely (if ever) allocates new backing stores, and because `Slice` is a plain object rather than a typed array, resizing or freeing a slice doesn't produce garbage for V8 to collect. It pre-allocates a large contiguous buffer and hands out regions using power-of-2 bucketing. When a slice is freed, its slot is recycled. When a slice is resized within the same bucket, no data moves at all — just a field update. This gives you `malloc`/`realloc`/`free` semantics with near-zero overhead per operation. The trade-off is upfront memory allocation and internal fragmentation from power-of-2 rounding — a 10-byte allocation uses a 16-byte slot. Buckets are also independent: a freed 16-byte slot cannot satisfy a 32-byte request, so the pool can become fragmented if allocation sizes are uneven. Use `stats` to monitor pool utilization and tune the pool size for your workload. | ||
| `PoolAllocator` takes this further. Like Node's internal pool, it has management overhead — but for in-pool sizes it never allocates new backing stores (only allocations larger than the 256 KB top bucket, or made once the contiguous pool is exhausted, fall back to a standalone `Buffer`), and because `Slice` is a plain object rather than a typed array, resizing or freeing a slice doesn't produce garbage for V8 to collect. It pre-allocates a large contiguous buffer and hands out regions using power-of-2 bucketing. When a slice is freed, its slot is recycled. When a slice is resized within the same bucket, no data moves at all — just a field update. This gives you `malloc`/`realloc`/`free` semantics with near-zero overhead per operation. The trade-off is upfront memory allocation and internal fragmentation from power-of-2 rounding — a 10-byte allocation uses a 16-byte slot. Buckets are also independent: a freed 16-byte slot cannot satisfy a 32-byte request, so the pool can become fragmented if allocation sizes are uneven. Use `stats` to monitor pool utilization and tune the pool size for your workload. | ||
@@ -106,7 +106,7 @@ ## Install | ||
| - `reset(): void` — Clear the slice back to empty state. **Note:** this does not return the slot to the `PoolAllocator` — you must call `realloc(slice, 0)` to free pool memory. | ||
| - `copy(target: Buffer | Slice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number` — Copy data to a `Buffer` or `Slice`. Returns bytes copied. | ||
| - `compare(target: Buffer | Slice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1` — Compare with a `Buffer` or `Slice` | ||
| - `copy(target: Uint8Array | Slice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number` — Copy data to a `Uint8Array`/`Buffer` or `Slice`. Returns bytes copied. | ||
| - `compare(target: Uint8Array | Slice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1` — Compare with a `Uint8Array`/`Buffer` or `Slice` | ||
| - `write(string: string, offset?: number, length?: number, encoding?: BufferEncoding): number` — Write a string into the slice. Returns bytes written. | ||
| - `set(source: Buffer | Slice | null | undefined, offset?: number): void` — Copy from a `Buffer` or `Slice` into this slice | ||
| - `at(index: number): number` — Read byte at index (supports negative indexing) | ||
| - `set(source: Buffer | Slice | null | undefined, offset?: number): void` — Copy from a `Buffer` or `Slice` into this slice. (A plain `Uint8Array` source is not accepted — it has no `copy` method.) | ||
| - `at(index: number): number` — Read byte at integer index (supports negative indexing) | ||
| - `test(expr: { test(buffer: Buffer, byteOffset: number, byteLength: number): boolean }): boolean` — Test the slice against an expression object | ||
@@ -116,2 +116,11 @@ - `toString(encoding?: BufferEncoding, start?: number, end?: number): string` — Convert to string | ||
| #### Validation & bounds | ||
| All offsets are **relative to the slice** (i.e. `0` is `byteOffset`). For a `Slice` target, target offsets are relative to that slice; for a raw `Buffer`/`Uint8Array` target they are absolute (passed straight through to the underlying `Buffer` method). | ||
| The rule is consistent across the API: | ||
| - **Start/offset arguments are validated** — `set`'s `offset`, `copy`/`compare`'s `sourceStart`/`targetStart`, `toString`/`toBuffer`'s `start`, `write`'s `offset`/`length`, and `at`'s `index` must be in-range integers. Out-of-range or non-integer values throw `RangeError`. This prevents a negative offset from resolving to a position **before** the slice and reading/writing adjacent (pool) memory. | ||
| - **End arguments are clamped** — `sourceEnd`/`targetEnd`/`end` are clamped to the slice's logical length (matching `Buffer`'s lenient end-of-range behavior), so over-long ranges never read past the slice's end. | ||
| #### Static | ||
@@ -125,15 +134,28 @@ | ||
| #### `new PoolAllocator(poolTotal?: number)` | ||
| #### `new PoolAllocator(poolTotalOrBuffer?: number | Buffer | ArrayBufferView | ArrayBuffer | SharedArrayBuffer)` | ||
| Creates a pool allocator. Default pool size is 128 MB. | ||
| Creates a pool allocator. Pass a byte size to allocate a fresh backing buffer (default 128 MB, must be a non-negative integer), or pass an existing `Buffer`/`ArrayBufferView`/`ArrayBuffer`/`SharedArrayBuffer` to back the pool with caller-provided memory. | ||
| > **Single-owner.** The allocator's bookkeeping lives in the instance, not in the backing buffer. When you supply your own buffer, that buffer must be owned exclusively by this allocator: do not build your own `Slice` views over it, do not share it with a second `PoolAllocator`, and (for a `SharedArrayBuffer`) do not allocate from more than one thread — the metadata is not shared or atomic, so doing any of these silently produces overlapping allocations. | ||
| #### Methods | ||
| - `realloc(slice: Slice, byteLength: number): Slice` — Allocate, resize, or free a slice. Pass `0` to free. | ||
| - `isFromPool(slice: Slice | null | undefined): boolean` — Check if a slice was allocated from this pool | ||
| - `realloc(byteLength: number): Slice` — Allocate a fresh slice. | ||
| - `realloc(slice: Slice, byteLength: number): Slice` — Resize a slice, or free it by passing `0`. **Contents are not preserved** — `realloc` has `malloc` semantics, not C `realloc` semantics; after a resize the bytes are undefined (a same-bucket resize happens to keep them in place, but do not rely on it). Only call `realloc` with a slice that belongs to this allocator (or a fresh/empty `Slice`); passing a slice from another pool, or freeing the same slice twice, corrupts the allocator's accounting. | ||
| - `isFromPool(slice: Slice | null | undefined): boolean` — Check if a slice's buffer is this pool's backing buffer. Note this is an identity check; it returns `true` for any slice over the same buffer, not only ones this allocator handed out. | ||
| Allocations larger than 256 KB (the largest bucket), or made when the contiguous pool is exhausted, fall back to a fresh standalone `Buffer` (`isFromPool` returns `false`) and are excluded from `size`/`stats`. | ||
| #### Properties | ||
| - `size: number` — Total size of all active allocations | ||
| - `stats: { size: number, padding: number, ratio: number, poolTotal: number, poolUsed: number, poolSize: number, poolCount: number }` — Detailed allocation statistics | ||
| - `size: number` — Total reserved bytes of all active **pool** allocations (sum of bucket sizes; equals `stats.poolSize`). | ||
| - `stats` — Detailed allocation statistics: | ||
| - `size` — same as the `size` getter (active pool bytes, including power-of-2 padding). | ||
| - `padding` — bytes lost to power-of-2 rounding across active pool slices. | ||
| - `ratio` — `size / (size - padding)`; `1` when there is no padding. | ||
| - `poolTotal` — capacity of the backing buffer in bytes. | ||
| - `poolUsed` — bump-pointer high-water mark; monotonic, never decreases. | ||
| - `poolSize` — active pool bytes (same as `size`). | ||
| - `poolCount` — number of distinct slots ever bump-allocated (monotonic high-water count, not a live count). | ||
| - `buckets` — per power-of-2 bucket: `{ free, used, size }`. | ||
@@ -140,0 +162,0 @@ ## License |
29637
24.26%481
12.38%161
15.83%