@nxtedition/slice 1.1.9 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ Node.js `Buffer.subarray()` is slow. Every call creates a new `Buffer` object
10
10
 
11
11
  `Slice` avoids this entirely. It is a plain JavaScript object with `buffer`, `byteOffset`, and `byteLength` fields. Creating a slice is just setting three properties — no typed array wrapper creation, no GC pressure from short-lived `Buffer` objects. Operations like `toString`, `copy`, and `compare` delegate directly to the underlying buffer with the correct offsets.
12
12
 
13
- `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.
13
+ `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.
14
14
 
15
15
  ## Install
16
16
 
@@ -102,15 +102,24 @@ Creates a new slice. All parameters are optional — defaults to an empty slice.
102
102
  #### Methods
103
103
 
104
104
  - `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.
105
- - `copy(target: Buffer | Slice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number` — Copy data to a `Buffer` or `Slice`. Returns bytes copied.
106
- - `compare(target: Buffer | Slice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1` — Compare with a `Buffer` or `Slice`
105
+ - `copy(target: Uint8Array | Slice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number` — Copy data to a `Uint8Array`/`Buffer` or `Slice`. Returns bytes copied.
106
+ - `compare(target: Uint8Array | Slice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1` — Compare with a `Uint8Array`/`Buffer` or `Slice`
107
107
  - `write(string: string, offset?: number, length?: number, encoding?: BufferEncoding): number` — Write a string into the slice. Returns bytes written.
108
- - `set(source: Buffer | Slice | null | undefined, offset?: number): void` — Copy from a `Buffer` or `Slice` into this slice
109
- - `at(index: number): number` — Read byte at index (supports negative indexing)
108
+ - `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.)
109
+ - `at(index: number): number` — Read byte at integer index (supports negative indexing)
110
110
  - `test(expr: { test(buffer: Buffer, byteOffset: number, byteLength: number): boolean }): boolean` — Test the slice against an expression object
111
111
  - `toString(encoding?: BufferEncoding, start?: number, end?: number): string` — Convert to string
112
112
  - `toBuffer(start?: number, end?: number): Buffer` — Return a `Buffer` view
113
113
 
114
+ #### Validation & bounds
115
+
116
+ 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).
117
+
118
+ The rule is consistent across the API:
119
+
120
+ - **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.
121
+ - **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.
122
+
114
123
  #### Static
115
124
 
116
125
  - `Slice.EMPTY_BUF: Buffer` — Shared empty buffer singleton
@@ -119,19 +128,32 @@ Creates a new slice. All parameters are optional — defaults to an empty slice.
119
128
 
120
129
  Pre-allocates a contiguous memory pool and manages slices using power-of-2 bucketing.
121
130
 
122
- #### `new PoolAllocator(poolTotal?: number)`
131
+ #### `new PoolAllocator(poolTotalOrBuffer?: number | Buffer | ArrayBufferView | ArrayBuffer | SharedArrayBuffer)`
132
+
133
+ 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.
123
134
 
124
- Creates a pool allocator. Default pool size is 128 MB.
135
+ > **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.
125
136
 
126
137
  #### Methods
127
138
 
128
- - `realloc(slice: Slice, byteLength: number): Slice` — Allocate, resize, or free a slice. Pass `0` to free.
129
- - `isFromPool(slice: Slice | null | undefined): boolean` — Check if a slice was allocated from this pool
139
+ - `realloc(byteLength: number): Slice` — Allocate a fresh slice.
140
+ - `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.
141
+ - `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.
142
+
143
+ 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`.
130
144
 
131
145
  #### Properties
132
146
 
133
- - `size: number` — Total size of all active allocations
134
- - `stats: { size: number, padding: number, ratio: number, poolTotal: number, poolUsed: number, poolSize: number, poolCount: number }` — Detailed allocation statistics
147
+ - `size: number` — Total reserved bytes of all active **pool** allocations (sum of bucket sizes; equals `stats.poolSize`).
148
+ - `stats` — Detailed allocation statistics:
149
+ - `size` — same as the `size` getter (active pool bytes, including power-of-2 padding).
150
+ - `padding` — bytes lost to power-of-2 rounding across active pool slices.
151
+ - `ratio` — `size / (size - padding)`; `1` when there is no padding.
152
+ - `poolTotal` — capacity of the backing buffer in bytes.
153
+ - `poolUsed` — bump-pointer high-water mark; monotonic, never decreases.
154
+ - `poolSize` — active pool bytes (same as `size`).
155
+ - `poolCount` — number of distinct slots ever bump-allocated (monotonic high-water count, not a live count).
156
+ - `buckets` — per power-of-2 bucket: `{ free, used, size }`.
135
157
 
136
158
  ## License
137
159
 
package/lib/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export type SliceLike = {
4
4
  byteOffset: number;
5
5
  byteLength: number;
6
6
  };
7
- export declare class Slice {
7
+ export declare class Slice implements SliceLike {
8
8
  buffer: Buffer;
9
9
  byteOffset: number;
10
10
  byteLength: number;
package/lib/index.js CHANGED
@@ -8,7 +8,7 @@ const EMPTY_BUF = Buffer.alloc(0)
8
8
 
9
9
 
10
10
 
11
- export class Slice {
11
+ export class Slice {
12
12
  buffer = EMPTY_BUF
13
13
  byteOffset = 0
14
14
  byteLength = 0
@@ -74,6 +74,12 @@ export class Slice {
74
74
  if (sourceStart === undefined) {
75
75
  sourceStart = this.byteOffset
76
76
  } else {
77
+ // Start offsets are validated (not clamped) so a negative offset can
78
+ // never resolve to a position before the slice and read adjacent
79
+ // (pool) memory.
80
+ if (sourceStart < 0 || sourceStart > this.byteLength || !Number.isInteger(sourceStart)) {
81
+ throw new RangeError(`Invalid sourceStart: ${sourceStart}`)
82
+ }
77
83
  sourceStart += this.byteOffset
78
84
  }
79
85
 
@@ -83,17 +89,23 @@ export class Slice {
83
89
  sourceEnd += this.byteOffset
84
90
  }
85
91
 
86
- // Clamp against the logical slice length so copy() cannot read past
87
- // the slice's own end and leak adjacent (pool) memory.
92
+ // Clamp the end against the logical slice length so copy() cannot read
93
+ // past the slice's own end and leak adjacent (pool) memory.
88
94
  if (sourceEnd > sliceEnd) {
89
95
  sourceEnd = sliceEnd
90
96
  }
97
+ if (sourceEnd < sourceStart) {
98
+ sourceEnd = sourceStart
99
+ }
91
100
 
92
101
  if (target instanceof Slice) {
93
102
  const targetEnd = target.byteOffset + target.byteLength
94
103
  if (targetStart === undefined) {
95
104
  targetStart = target.byteOffset
96
105
  } else {
106
+ if (targetStart < 0 || targetStart > target.byteLength || !Number.isInteger(targetStart)) {
107
+ throw new RangeError(`Invalid targetStart: ${targetStart}`)
108
+ }
97
109
  targetStart += target.byteOffset
98
110
  }
99
111
 
@@ -138,6 +150,9 @@ export class Slice {
138
150
  if (targetStart === undefined) {
139
151
  targetStart = target.byteOffset
140
152
  } else {
153
+ if (targetStart < 0 || targetStart > target.byteLength || !Number.isInteger(targetStart)) {
154
+ throw new RangeError(`Invalid targetStart: ${targetStart}`)
155
+ }
141
156
  targetStart += target.byteOffset
142
157
  }
143
158
 
@@ -150,12 +165,18 @@ export class Slice {
150
165
  if (targetEnd > targetSliceEnd) {
151
166
  targetEnd = targetSliceEnd
152
167
  }
168
+ if (targetEnd < targetStart) {
169
+ targetEnd = targetStart
170
+ }
153
171
  target = target.buffer
154
172
  }
155
173
 
156
174
  if (sourceStart === undefined) {
157
175
  sourceStart = this.byteOffset
158
176
  } else {
177
+ if (sourceStart < 0 || sourceStart > this.byteLength || !Number.isInteger(sourceStart)) {
178
+ throw new RangeError(`Invalid sourceStart: ${sourceStart}`)
179
+ }
159
180
  sourceStart += this.byteOffset
160
181
  }
161
182
 
@@ -168,6 +189,9 @@ export class Slice {
168
189
  if (sourceEnd > sliceEnd) {
169
190
  sourceEnd = sliceEnd
170
191
  }
192
+ if (sourceEnd < sourceStart) {
193
+ sourceEnd = sourceStart
194
+ }
171
195
 
172
196
  return this.buffer.compare(target, targetStart, targetEnd, sourceStart, sourceEnd)
173
197
  }
@@ -178,7 +202,7 @@ export class Slice {
178
202
  if (offset === undefined) {
179
203
  offset = this.byteOffset
180
204
  } else {
181
- if (offset < 0 || offset > this.byteLength) {
205
+ if (offset < 0 || offset > this.byteLength || !Number.isInteger(offset)) {
182
206
  throw new RangeError(`Invalid offset: ${offset}`)
183
207
  }
184
208
  offset += this.byteOffset
@@ -187,8 +211,13 @@ export class Slice {
187
211
  const available = sliceEnd - offset
188
212
  if (length === undefined) {
189
213
  length = available
190
- } else if (length > available) {
191
- length = available
214
+ } else {
215
+ if (length < 0 || !Number.isInteger(length)) {
216
+ throw new RangeError(`Invalid length: ${length}`)
217
+ }
218
+ if (length > available) {
219
+ length = available
220
+ }
192
221
  }
193
222
 
194
223
  return this.buffer.write(string, offset, length, encoding)
@@ -202,6 +231,11 @@ export class Slice {
202
231
  if (offset === undefined) {
203
232
  offset = this.byteOffset
204
233
  } else {
234
+ // Validate (not clamp): a negative offset would resolve to a position
235
+ // before the slice and corrupt adjacent (pool) memory.
236
+ if (offset < 0 || offset > this.byteLength || !Number.isInteger(offset)) {
237
+ throw new RangeError(`Invalid offset: ${offset}`)
238
+ }
205
239
  offset += this.byteOffset
206
240
  }
207
241
 
@@ -215,7 +249,7 @@ export class Slice {
215
249
  }
216
250
 
217
251
  at(index ) {
218
- if (index >= this.byteLength || index < -this.byteLength) {
252
+ if (!Number.isInteger(index) || index >= this.byteLength || index < -this.byteLength) {
219
253
  throw new RangeError(`Index out of range: ${index}`)
220
254
  }
221
255
  return index >= 0
@@ -235,6 +269,9 @@ export class Slice {
235
269
  if (start === undefined) {
236
270
  start = this.byteOffset
237
271
  } else {
272
+ if (start < 0 || start > this.byteLength || !Number.isInteger(start)) {
273
+ throw new RangeError(`Invalid start: ${start}`)
274
+ }
238
275
  start += this.byteOffset
239
276
  }
240
277
 
@@ -247,6 +284,9 @@ export class Slice {
247
284
  if (end > sliceEnd) {
248
285
  end = sliceEnd
249
286
  }
287
+ if (end < start) {
288
+ end = start
289
+ }
250
290
 
251
291
  return this.buffer.toString(encoding, start, end)
252
292
  }
@@ -257,6 +297,9 @@ export class Slice {
257
297
  if (start === undefined) {
258
298
  start = this.byteOffset
259
299
  } else {
300
+ if (start < 0 || start > this.byteLength || !Number.isInteger(start)) {
301
+ throw new RangeError(`Invalid start: ${start}`)
302
+ }
260
303
  start += this.byteOffset
261
304
  }
262
305
 
@@ -269,6 +312,9 @@ export class Slice {
269
312
  if (end > sliceEnd) {
270
313
  end = sliceEnd
271
314
  }
315
+ if (end < start) {
316
+ end = start
317
+ }
272
318
 
273
319
  return start === 0 && end === this.buffer.byteLength
274
320
  ? this.buffer
@@ -282,7 +328,13 @@ export class Slice {
282
328
  [util.inspect.custom]() {
283
329
  const MAX_BYTES = 32
284
330
  const len = this.byteLength
285
- const shown = len < MAX_BYTES ? len : MAX_BYTES
331
+
332
+ // Never read past the underlying buffer. A malformed slice (byteOffset /
333
+ // byteLength extending beyond the buffer) is exactly the state a developer
334
+ // inspects while debugging pool corruption — inspect must not throw on it.
335
+ const available = this.buffer.byteLength - this.byteOffset
336
+ const safeLen = available > 0 ? (len < available ? len : available) : 0
337
+ const shown = safeLen < MAX_BYTES ? safeLen : MAX_BYTES
286
338
 
287
339
  let hex = ''
288
340
  for (let i = 0; i < shown; i++) {
@@ -295,8 +347,7 @@ export class Slice {
295
347
  hex += ` ... (${len - shown} more)`
296
348
  }
297
349
 
298
- const strEnd = shown < len ? this.byteOffset + shown : this.byteOffset + len
299
- const str = this.buffer.toString('utf8', this.byteOffset, strEnd)
350
+ const str = this.buffer.toString('utf8', this.byteOffset, this.byteOffset + shown)
300
351
  const truncated = shown < len ? '…' : ''
301
352
 
302
353
  return `Slice(${len}): "${str}${truncated}" <${hex}>`
@@ -320,6 +371,9 @@ export class PoolAllocator {
320
371
  1024,
321
372
  ) {
322
373
  if (typeof poolTotalOrBuffer === 'number') {
374
+ if (!Number.isInteger(poolTotalOrBuffer) || poolTotalOrBuffer < 0) {
375
+ throw new RangeError(`Invalid pool size: ${poolTotalOrBuffer}`)
376
+ }
323
377
  this.#poolBuffer = Buffer.allocUnsafeSlow(poolTotalOrBuffer)
324
378
  } else if (poolTotalOrBuffer instanceof Buffer) {
325
379
  this.#poolBuffer = poolTotalOrBuffer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/slice",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "type": "module",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -30,5 +30,5 @@
30
30
  "tsd": "^0.33.0",
31
31
  "typescript": "^5.9.3"
32
32
  },
33
- "gitHead": "2c131da7ca8ea328fd681e975dd7caf57f9f4896"
33
+ "gitHead": "7c9c7457c885c644c7a1e70ef894d4727ce240d6"
34
34
  }