@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 +33 -11
- package/lib/index.d.ts +1 -1
- package/lib/index.js +64 -10
- package/package.json +2 -2
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
|
|
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:
|
|
106
|
-
- `compare(target:
|
|
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(
|
|
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
|
-
|
|
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(
|
|
129
|
-
- `
|
|
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
|
|
134
|
-
- `stats
|
|
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
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
|
|
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
|
|
191
|
-
length
|
|
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
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
33
|
+
"gitHead": "7c9c7457c885c644c7a1e70ef894d4727ce240d6"
|
|
34
34
|
}
|