Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use DataView instead of ArrayBuffer, fixes #3 #4

Merged
merged 1 commit into from
Jul 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 21 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,52 +15,45 @@ use in Browsers the bundler will inject a rather large polyfill for the entire
avoiding this penalty.

However, there is some good news. No matter what the binary type there's an underlying
`ArrayBuffer` associated with the instance. This means that you can *mostly* do **zero copy**
conversions of any of these types to `ArrayBuffer` and back.
`ArrayBuffer` associated with the instance. There's also one generic binary view object
available in both Node.js and Browsers called `DataView`. This means that you can take
any binary type and do a **zero memcopy** conversion to a `DataView`.

But there are some problems with `DataView`. Not all APIs take it in browsers and almost
none accept it in Node.js. It's a great API for reading and writing to an `ArrayBuffer`
but it lacks a lot of other functionality that can be difficult to accomplish cross-platform.

`bytesish` is here to help. This library helps you accept and convert different binary types
into a consistent type without loading any polyfills or other dependencies, then
into a consistent type, `DataView`, without loading any polyfills or other dependencies, then
convert back into an ideal type for the platform your library is running in.

What `bytesish` does:

* Returns an array buffer from any known binary type (*mostly* zero copy).
* Creates an ArrayBuffer from a string with any encoding.
* Converts an ArrayBuffer to a string of any encoding.
* Converts an ArrayBuffer to an ideal native object (`Buffer` or `ArrayBuffer`).
* Converts an ArrayBuffer to an ideal native object (`Buffer` or `Uint8Array`).

`bytesish` does not create a new Binary API or interface for accessing and manipulating
binary data, because you can just use `ArrayBuffer` for that. `bytesish` tries to be a
`bytesish` does not create a new Binary Type for basic accessing and manipulating of
binary data, because you can just use `DataView` for that. `bytesish` tries to be a
small piece of code that does not contribute any more than necessary to your bundle size.
It does this by containing only the binary operations you need that are difficult to
do cross-platform (Node.js and Browsers).

```javascript
let bytes = require('bytesish')
let arrayBuffer = bytes('hello world')
let view = bytes('hello world')

/* *mostly* zero copy conversions */
arrayBuffer = bytes(Buffer.from('hello world')) // Buffer instance
arrayBuffer = bytes((new TextEncoder()).encode('hello world')) // Uint8Array
/* zero copy conversions */
view = bytes(Buffer.from('hello world')) // Buffer instance
view = bytes((new TextEncoder()).encode('hello world')) // Uint8Array

/* base64 conversions */
let base64String = bytes.toString(arrayBuffer, 'base64')
let arrayBufferCopy = bytes(base64String, 'base64')
```

## Gotchas
let base64String = bytes.toString(view, 'base64')
base64String = bytes.toString(Buffer.from('hello world'), 'base64')
base64String = bytes.toString('hello world', 'base64')

Most Browser binary types are either ArrayBuffer's or views of a single ArrayBuffer, so
all of them can be converted to an ArrayBuffer without a memcopy, but it is possible
to make a TypedArray view of a slice of an ArrayBuffer. In Node.js the
Buffer API is *sometimes* a view of a single ArrayBuffer and *sometimes* a view of a
larger ArrayBuffer. When one is used and not the other has a lot to do with the size
of the buffer (this only happens with small buffers) and how the buffer was created.

Since `bytesish` always create clean `Buffer` instances over a discreet `ArrayBuffer`,
you'll only ever suffer a memcopy **once** if you encounter one of these `Buffer`
instances in Node.js. The same happens if a `TypeArray` view of a slice is converted.
From that point on, not matter how many calls and conversions
happen, you should never suffer another memcopy since `bytesish` can always tell
that the native `Buffer` objects being sent are for a single `ArrayBuffer`.
/* since this is a string conversion it will create a new binary instance */
let viewCopy = bytes(base64String, 'base64')
```

25 changes: 17 additions & 8 deletions browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,45 @@
const bytes = require('./core')

bytes.from = (_from, _encoding) => {
if (_from instanceof ArrayBuffer) return _from
if (_from instanceof DataView) return _from
if (_from instanceof ArrayBuffer) return new DataView(_from)
let buffer
if (typeof _from === 'string') {
if (!_encoding) {
_encoding = 'utf-8'
} else if (_encoding === 'base64') {
buffer = Uint8Array.from(atob(_from), c => c.charCodeAt(0)).buffer
return buffer
return new DataView(buffer)
}
if (_encoding !== 'utf-8') throw new Error('Browser support for encodings other than utf-8 not implemented')
return (new TextEncoder()).encode(_from).buffer
return new DataView((new TextEncoder()).encode(_from).buffer)
} else if (typeof _from === 'object') {
if (ArrayBuffer.isView(_from)) {
if (_from.byteLength === _from.buffer.byteLength) return _from.buffer
else return _from.buffer.slice(_from.byteOffset, _from.byteOffset + _from.byteLength)
if (_from.byteLength === _from.buffer.byteLength) return new DataView(_from.buffer)
else return new DataView(_from.buffer, _from.byteOffset, _from.byteLength)
}
}
throw new Error('Unkown type. Cannot convert to ArrayBuffer')
}

bytes.toString = (_from, encoding) => {
_from = bytes.from(_from, encoding)
const str = String.fromCharCode(...new Uint8Array(_from))
_from = bytes(_from, encoding)
const uint = new Uint8Array(_from.buffer, _from.byteOffset, _from.byteLength)
const str = String.fromCharCode(...uint)
if (encoding === 'base64') {
/* would be nice to find a way to do this directly from a buffer
* instead of doing two string conversions
*/
return btoa(str)
} else {
return str
}
}

bytes.native = arg => bytes.from(arg)
bytes.native = arg => {
if (arg instanceof Uint8Array) return arg
arg = bytes.from(arg)
return new Uint8Array(arg.buffer, arg.byteOffset, arg.byteLength)
}

module.exports = bytes
4 changes: 2 additions & 2 deletions core.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const length = (a, b) => {
const bytes = (_from, encoding) => bytes.from(_from, encoding)

bytes.sort = (a, b) => {
a = new DataView(bytes(a))
b = new DataView(bytes(b))
a = bytes(a)
b = bytes(b)
const len = length(a, b)
let i = 0
while (i < (len - 1)) {
Expand Down
16 changes: 8 additions & 8 deletions node.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@ const fallback = require('./browser').from
const bytes = require('./core')

bytes.from = (_from, encoding) => {
if (_from instanceof ArrayBuffer) return _from
if (_from instanceof DataView) return _from
if (_from instanceof ArrayBuffer) return new DataView(_from)
if (typeof _from === 'string') {
return Buffer.alloc(Buffer.byteLength(_from), _from, encoding).buffer
_from = Buffer.from(_from, encoding)
}
if (Buffer.isBuffer(_from)) {
// This Buffer is not a view of a larger ArrayBuffer
if (_from.buffer.byteLength === _from.length) return _from.buffer
// This Buffer *is* a view of a larger ArrayBuffer so we have to copy it
else return _from.buffer.slice(_from.byteOffset, _from.byteOffset + _from.byteLength)
return new DataView(_from.buffer, _from.byteOffset, _from.byteLength)
}
return fallback(_from, encoding)
}
bytes.toString = (_from, encoding) => {
return Buffer.from(bytes.from(_from)).toString(encoding)
_from = bytes(_from)
return Buffer.from(_from.buffer, _from.byteOffset, _from.byteLength).toString(encoding)
}

bytes.native = arg => {
if (Buffer.isBuffer(arg)) return arg
Buffer.from(bytes.from(arg))
arg = bytes(arg)
return Buffer.from(arg.buffer, arg.byteOffset, arg.byteLength)
}

module.exports = bytes
1 change: 1 addition & 0 deletions test/basics.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const same = (x, y) => assert.ok(tsame(x, y))

test('string conversion', done => {
const ab = bytes('hello world')
assert(ab instanceof DataView)
const str = bytes.toString(ab)
same(str, 'hello world')
done()
Expand Down