@nxtedition/slice 1.1.8 → 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,27 +4,13 @@ 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;
11
11
  maxByteLength: number;
12
12
  static get EMPTY_BUF(): Buffer;
13
- /**
14
- * Fast factory for the common case of wrapping a full Buffer with no
15
- * offset and no separate maxByteLength. Skips all the constructor's
16
- * validation (Buffer instanceof check, byteOffset/byteLength range
17
- * checks, integer checks, maxByteLength bounds). Use only when the
18
- * caller already knows the input is a Buffer — e.g. wrapping freshly
19
- * received network frames on a hot path.
20
- *
21
- * Note: instances produced here have a slightly different V8 hidden
22
- * class than instances produced via `new Slice(buf)`. That introduces
23
- * polymorphism at any downstream IC that reads slice fields. Measure
24
- * end-to-end before adopting in code paths where consumers do heavy
25
- * field access.
26
- */
27
- static fromBuffer(buffer: Buffer): Slice;
13
+ static from(buffer: Buffer, byteOffset?: number, byteLength?: number): Slice;
28
14
  constructor(buffer?: Buffer<ArrayBufferLike>, byteOffset?: number, byteLength?: number, maxByteLength?: number);
29
15
  reset(): void;
30
16
  get length(): 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
@@ -18,36 +18,7 @@ export class Slice {
18
18
  return EMPTY_BUF
19
19
  }
20
20
 
21
- /**
22
- * Fast factory for the common case of wrapping a full Buffer with no
23
- * offset and no separate maxByteLength. Skips all the constructor's
24
- * validation (Buffer instanceof check, byteOffset/byteLength range
25
- * checks, integer checks, maxByteLength bounds). Use only when the
26
- * caller already knows the input is a Buffer — e.g. wrapping freshly
27
- * received network frames on a hot path.
28
- *
29
- * Note: instances produced here have a slightly different V8 hidden
30
- * class than instances produced via `new Slice(buf)`. That introduces
31
- * polymorphism at any downstream IC that reads slice fields. Measure
32
- * end-to-end before adopting in code paths where consumers do heavy
33
- * field access.
34
- */
35
- static fromBuffer(buffer ) {
36
- const slice = Object.create(Slice.prototype)
37
- const len = buffer.byteLength
38
- slice.buffer = buffer
39
- slice.byteOffset = 0
40
- slice.byteLength = len
41
- slice.maxByteLength = len
42
- return slice
43
- }
44
-
45
- constructor(
46
- buffer = Slice.EMPTY_BUF,
47
- byteOffset = 0,
48
- byteLength = buffer.byteLength,
49
- maxByteLength = byteLength,
50
- ) {
21
+ static from(buffer , byteOffset = 0, byteLength = buffer.byteLength - byteOffset) {
51
22
  if (!(buffer instanceof Buffer)) {
52
23
  throw new TypeError('buffer must be a Buffer')
53
24
  }
@@ -60,20 +31,21 @@ export class Slice {
60
31
  throw new RangeError(`Invalid byteLength: ${byteLength}`)
61
32
  }
62
33
 
63
- if (
64
- maxByteLength < byteLength ||
65
- maxByteLength > buffer.byteLength ||
66
- !Number.isInteger(maxByteLength)
67
- ) {
68
- throw new RangeError(`Invalid maxByteLength: ${maxByteLength}`)
69
- }
70
-
71
34
  if (byteOffset + byteLength > buffer.byteLength) {
72
35
  throw new RangeError(
73
36
  `byteOffset + byteLength (${byteOffset + byteLength}) exceeds buffer size (${buffer.byteLength})`,
74
37
  )
75
38
  }
76
39
 
40
+ return new Slice(buffer, byteOffset, byteLength)
41
+ }
42
+
43
+ constructor(
44
+ buffer = Slice.EMPTY_BUF,
45
+ byteOffset = 0,
46
+ byteLength = buffer.byteLength,
47
+ maxByteLength = byteLength,
48
+ ) {
77
49
  this.buffer = buffer
78
50
  this.byteOffset = byteOffset
79
51
  this.byteLength = byteLength
@@ -102,6 +74,12 @@ export class Slice {
102
74
  if (sourceStart === undefined) {
103
75
  sourceStart = this.byteOffset
104
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
+ }
105
83
  sourceStart += this.byteOffset
106
84
  }
107
85
 
@@ -111,17 +89,23 @@ export class Slice {
111
89
  sourceEnd += this.byteOffset
112
90
  }
113
91
 
114
- // Clamp against the logical slice length so copy() cannot read past
115
- // 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.
116
94
  if (sourceEnd > sliceEnd) {
117
95
  sourceEnd = sliceEnd
118
96
  }
97
+ if (sourceEnd < sourceStart) {
98
+ sourceEnd = sourceStart
99
+ }
119
100
 
120
101
  if (target instanceof Slice) {
121
102
  const targetEnd = target.byteOffset + target.byteLength
122
103
  if (targetStart === undefined) {
123
104
  targetStart = target.byteOffset
124
105
  } else {
106
+ if (targetStart < 0 || targetStart > target.byteLength || !Number.isInteger(targetStart)) {
107
+ throw new RangeError(`Invalid targetStart: ${targetStart}`)
108
+ }
125
109
  targetStart += target.byteOffset
126
110
  }
127
111
 
@@ -166,6 +150,9 @@ export class Slice {
166
150
  if (targetStart === undefined) {
167
151
  targetStart = target.byteOffset
168
152
  } else {
153
+ if (targetStart < 0 || targetStart > target.byteLength || !Number.isInteger(targetStart)) {
154
+ throw new RangeError(`Invalid targetStart: ${targetStart}`)
155
+ }
169
156
  targetStart += target.byteOffset
170
157
  }
171
158
 
@@ -178,12 +165,18 @@ export class Slice {
178
165
  if (targetEnd > targetSliceEnd) {
179
166
  targetEnd = targetSliceEnd
180
167
  }
168
+ if (targetEnd < targetStart) {
169
+ targetEnd = targetStart
170
+ }
181
171
  target = target.buffer
182
172
  }
183
173
 
184
174
  if (sourceStart === undefined) {
185
175
  sourceStart = this.byteOffset
186
176
  } else {
177
+ if (sourceStart < 0 || sourceStart > this.byteLength || !Number.isInteger(sourceStart)) {
178
+ throw new RangeError(`Invalid sourceStart: ${sourceStart}`)
179
+ }
187
180
  sourceStart += this.byteOffset
188
181
  }
189
182
 
@@ -196,6 +189,9 @@ export class Slice {
196
189
  if (sourceEnd > sliceEnd) {
197
190
  sourceEnd = sliceEnd
198
191
  }
192
+ if (sourceEnd < sourceStart) {
193
+ sourceEnd = sourceStart
194
+ }
199
195
 
200
196
  return this.buffer.compare(target, targetStart, targetEnd, sourceStart, sourceEnd)
201
197
  }
@@ -206,7 +202,7 @@ export class Slice {
206
202
  if (offset === undefined) {
207
203
  offset = this.byteOffset
208
204
  } else {
209
- if (offset < 0 || offset > this.byteLength) {
205
+ if (offset < 0 || offset > this.byteLength || !Number.isInteger(offset)) {
210
206
  throw new RangeError(`Invalid offset: ${offset}`)
211
207
  }
212
208
  offset += this.byteOffset
@@ -215,8 +211,13 @@ export class Slice {
215
211
  const available = sliceEnd - offset
216
212
  if (length === undefined) {
217
213
  length = available
218
- } else if (length > available) {
219
- 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
+ }
220
221
  }
221
222
 
222
223
  return this.buffer.write(string, offset, length, encoding)
@@ -230,6 +231,11 @@ export class Slice {
230
231
  if (offset === undefined) {
231
232
  offset = this.byteOffset
232
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
+ }
233
239
  offset += this.byteOffset
234
240
  }
235
241
 
@@ -243,7 +249,7 @@ export class Slice {
243
249
  }
244
250
 
245
251
  at(index ) {
246
- if (index >= this.byteLength || index < -this.byteLength) {
252
+ if (!Number.isInteger(index) || index >= this.byteLength || index < -this.byteLength) {
247
253
  throw new RangeError(`Index out of range: ${index}`)
248
254
  }
249
255
  return index >= 0
@@ -263,6 +269,9 @@ export class Slice {
263
269
  if (start === undefined) {
264
270
  start = this.byteOffset
265
271
  } else {
272
+ if (start < 0 || start > this.byteLength || !Number.isInteger(start)) {
273
+ throw new RangeError(`Invalid start: ${start}`)
274
+ }
266
275
  start += this.byteOffset
267
276
  }
268
277
 
@@ -275,6 +284,9 @@ export class Slice {
275
284
  if (end > sliceEnd) {
276
285
  end = sliceEnd
277
286
  }
287
+ if (end < start) {
288
+ end = start
289
+ }
278
290
 
279
291
  return this.buffer.toString(encoding, start, end)
280
292
  }
@@ -285,6 +297,9 @@ export class Slice {
285
297
  if (start === undefined) {
286
298
  start = this.byteOffset
287
299
  } else {
300
+ if (start < 0 || start > this.byteLength || !Number.isInteger(start)) {
301
+ throw new RangeError(`Invalid start: ${start}`)
302
+ }
288
303
  start += this.byteOffset
289
304
  }
290
305
 
@@ -297,6 +312,9 @@ export class Slice {
297
312
  if (end > sliceEnd) {
298
313
  end = sliceEnd
299
314
  }
315
+ if (end < start) {
316
+ end = start
317
+ }
300
318
 
301
319
  return start === 0 && end === this.buffer.byteLength
302
320
  ? this.buffer
@@ -310,7 +328,13 @@ export class Slice {
310
328
  [util.inspect.custom]() {
311
329
  const MAX_BYTES = 32
312
330
  const len = this.byteLength
313
- 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
314
338
 
315
339
  let hex = ''
316
340
  for (let i = 0; i < shown; i++) {
@@ -323,8 +347,7 @@ export class Slice {
323
347
  hex += ` ... (${len - shown} more)`
324
348
  }
325
349
 
326
- const strEnd = shown < len ? this.byteOffset + shown : this.byteOffset + len
327
- const str = this.buffer.toString('utf8', this.byteOffset, strEnd)
350
+ const str = this.buffer.toString('utf8', this.byteOffset, this.byteOffset + shown)
328
351
  const truncated = shown < len ? '…' : ''
329
352
 
330
353
  return `Slice(${len}): "${str}${truncated}" <${hex}>`
@@ -348,6 +371,9 @@ export class PoolAllocator {
348
371
  1024,
349
372
  ) {
350
373
  if (typeof poolTotalOrBuffer === 'number') {
374
+ if (!Number.isInteger(poolTotalOrBuffer) || poolTotalOrBuffer < 0) {
375
+ throw new RangeError(`Invalid pool size: ${poolTotalOrBuffer}`)
376
+ }
351
377
  this.#poolBuffer = Buffer.allocUnsafeSlow(poolTotalOrBuffer)
352
378
  } else if (poolTotalOrBuffer instanceof Buffer) {
353
379
  this.#poolBuffer = poolTotalOrBuffer
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxtedition/slice",
3
- "version": "1.1.8",
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": "088f6e6d615d4eeee0b2e317c9011dfa57308cc9"
33
+ "gitHead": "7c9c7457c885c644c7a1e70ef894d4727ce240d6"
34
34
  }