Skip to content

Commit 5ca0f41

Browse files
committed
feat: typedCopyBytes
1 parent 8d54290 commit 5ca0f41

File tree

4 files changed

+122
-3
lines changed

4 files changed

+122
-3
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,16 +529,35 @@ Encode WIF data to a WIF string (synchronous)
529529
TypedArray utils and conversions.
530530

531531
```js
532-
import { typedView } from '@exodus/bytes/array.js'
532+
import { typedCopyBytes, typedView } from '@exodus/bytes/array.js'
533533
```
534534

535+
#### `typedCopyBytes(arr, format = 'uint8')`
536+
537+
Create a copy of TypedArray underlying bytes in the specified format (`'uint8'`, `'buffer'`, or `'arraybuffer'`)
538+
539+
This does not copy _values_, but copies the underlying bytes.
540+
The result is similar to that of `typedView()`, but this function provides a copy, not a view of the same memory.
541+
542+
> [!WARNING]
543+
> Copying underlying bytes from `Uint16Array` (or other with `BYTES_PER_ELEMENT > 1`)
544+
> is platform endianness-dependent.
545+
546+
> [!NOTE]
547+
> Buffer might be pooled.
548+
> Uint8Array return values are not pooled and match their underlying ArrayBuffer.
549+
535550
#### `typedView(arr, format = 'uint8')`
536551

537552
Create a view of a TypedArray in the specified format (`'uint8'` or `'buffer'`)
538553

539554
> [!IMPORTANT]
540555
> Does not copy data, returns a view on the same underlying buffer
541556
557+
> [!WARNING]
558+
> Viewing `Uint16Array` (or other with `BYTES_PER_ELEMENT > 1`) as bytes
559+
> is platform endianness-dependent.
560+
542561
### @exodus/bytes/encoding.js <sub>![](https://img.shields.io/bundlejs/size/@exodus/bytes/encoding.js?style=flat-square)</sub>
543562

544563
Implements the [Encoding standard](https://encoding.spec.whatwg.org/):

array.d.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* TypedArray utils and conversions.
33
*
44
* ```js
5-
* import { typedView } from '@exodus/bytes/array.js'
5+
* import { typedCopyBytes, typedView } from '@exodus/bytes/array.js'
66
* ```
77
*
88
* @module @exodus/bytes/array.js
@@ -47,16 +47,48 @@ export type Uint32ArrayBuffer = ReturnType<typeof Uint32Array.from>;
4747
*/
4848
export type OutputFormat = 'uint8' | 'buffer';
4949

50+
/**
51+
* Output format for typed array copy conversions
52+
*/
53+
export type OutputCopyFormat = 'uint8' | 'arraybuffer' | 'buffer';
54+
5055
/**
5156
* Create a view of a TypedArray in the specified format (`'uint8'` or `'buffer'`)
5257
*
5358
* > [!IMPORTANT]
5459
* > Does not copy data, returns a view on the same underlying buffer
5560
*
61+
* > [!WARNING]
62+
* > Viewing `Uint16Array` (or other with `BYTES_PER_ELEMENT > 1`) as bytes
63+
* > is platform endianness-dependent.
64+
*
5665
* @param arr - The input TypedArray
5766
* @param format - The desired output format (`'uint8'` or `'buffer'`)
5867
* @returns A view on the same underlying buffer
5968
*/
6069
export function typedView(arr: ArrayBufferView, format: 'uint8'): Uint8Array;
6170
export function typedView(arr: ArrayBufferView, format: 'buffer'): Buffer;
6271
export function typedView(arr: ArrayBufferView, format: OutputFormat): Uint8Array | Buffer;
72+
73+
/**
74+
* Create a copy of TypedArray underlying bytes in the specified format (`'uint8'`, `'buffer'`, or `'arraybuffer'`)
75+
*
76+
* This does not copy _values_, but copies the underlying bytes.
77+
* The result is similar to that of `typedView()`, but this function provides a copy, not a view of the same memory.
78+
*
79+
* > [!WARNING]
80+
* > Copying underlying bytes from `Uint16Array` (or other with `BYTES_PER_ELEMENT > 1`)
81+
* > is platform endianness-dependent.
82+
*
83+
* > [!NOTE]
84+
* > Buffer might be pooled.
85+
* > Uint8Array return values are not pooled and match their underlying ArrayBuffer.
86+
*
87+
* @param arr - The input TypedArray
88+
* @param format - The desired output format (`'uint8'`, `'buffer'`, or `'arraybuffer'`)
89+
* @returns A copy of the underlying buffer
90+
*/
91+
export function typedCopyBytes(arr: ArrayBufferView, format: 'uint8'): Uint8Array;
92+
export function typedCopyBytes(arr: ArrayBufferView, format: 'arraybuffer'): ArrayBuffer;
93+
export function typedCopyBytes(arr: ArrayBufferView, format: 'buffer'): Buffer;
94+
export function typedCopyBytes(arr: ArrayBufferView, format: OutputCopyFormat): Uint8Array | ArrayBuffer | Buffer;

array.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,21 @@ export function typedView(arr, format) {
1515

1616
throw new TypeError('Unexpected format')
1717
}
18+
19+
export function typedCopyBytes(arr, format) {
20+
assertTypedArray(arr)
21+
if (!(arr instanceof Uint8Array)) {
22+
arr = new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength)
23+
}
24+
25+
switch (format) {
26+
case 'uint8':
27+
return Uint8Array.from(arr) // never pooled
28+
case 'buffer':
29+
return Buffer.from(arr)
30+
case 'arraybuffer':
31+
return Uint8Array.from(arr).buffer
32+
}
33+
34+
throw new TypeError('Unexpected format')
35+
}

tests/array.test.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { typedView } from '@exodus/bytes/array.js'
1+
import { typedView, typedCopyBytes } from '@exodus/bytes/array.js'
22
import { describe, test } from 'node:test'
33

44
const raw = [new Uint8Array(), new Uint8Array([0]), new Uint8Array([1]), new Uint8Array([255])]
@@ -40,3 +40,53 @@ describe('typedView', () => {
4040
}
4141
})
4242
})
43+
44+
describe('typedCopyBytes', () => {
45+
test('invalid input', (t) => {
46+
for (const input of [null, undefined, [], [1, 2], 'string']) {
47+
t.assert.throws(() => typedCopyBytes(input))
48+
for (const form of ['uint8', 'buffer']) {
49+
t.assert.throws(() => typedCopyBytes(input, form))
50+
}
51+
}
52+
})
53+
54+
test('uint8', (t) => {
55+
for (const { buffer, uint8 } of pool) {
56+
for (const arg of [buffer, uint8]) {
57+
const a = typedCopyBytes(arg, 'uint8')
58+
t.assert.deepStrictEqual(a, uint8)
59+
t.assert.notEqual(a, arg)
60+
t.assert.strictEqual(a.byteLength, arg.byteLength)
61+
// Non-pooled
62+
t.assert.notEqual(a.buffer, arg.buffer)
63+
t.assert.strictEqual(a.byteLength, a.buffer.byteLength)
64+
}
65+
}
66+
})
67+
68+
test('arraybuffer', (t) => {
69+
for (const { buffer, uint8 } of pool) {
70+
for (const arg of [buffer, uint8]) {
71+
const a = typedCopyBytes(arg, 'arraybuffer')
72+
t.assert.deepStrictEqual(a, Uint8Array.from(arg).buffer)
73+
t.assert.notEqual(a, arg)
74+
t.assert.strictEqual(a.byteLength, arg.byteLength)
75+
t.assert.notEqual(a, arg.buffer)
76+
}
77+
}
78+
})
79+
80+
test('buffer', (t) => {
81+
for (const { uint8, buffer } of pool) {
82+
for (const arg of [buffer, uint8]) {
83+
const a = typedCopyBytes(arg, 'buffer')
84+
t.assert.deepStrictEqual(a, buffer)
85+
t.assert.notEqual(a, arg)
86+
t.assert.strictEqual(a.byteLength, arg.byteLength)
87+
// Might be pooled
88+
t.assert.ok(a.buffer !== arg.buffer || a.byteOffset !== arg.byteOffset)
89+
}
90+
}
91+
})
92+
})

0 commit comments

Comments
 (0)