ordered-binary 1.1.0 → 1.2.0

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
@@ -1,8 +1,34 @@
1
+ [![npm version](https://img.shields.io/npm/dw/ordered-binary)](https://www.npmjs.org/package/ordered-binary)
2
+ [![npm version](https://img.shields.io/npm/v/ordered-binary.svg?style=flat-square)](https://www.npmjs.org/package/ordered-binary)
3
+ [![license](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE)
1
4
  <a href="https://dev.doctorevidence.com/"><img src="./assets/powers-dre.png" width="203" /></a>
2
5
 
3
- The ordered-binary provides a representation of JavaScript primitives as NodeJS Buffers, such that there binary values are naturally ordered such that it matches primitive ordering. For example, since -2.0321 > -2.04, then `toBufferKey(-2.0321)` will be greater than `toBufferKey(-2.04)` as a binary representation, in left-to-right evaluation. This is particular useful for storing keys as binaries with something like LevelDB, to avoid any custom sorting.
6
+ The ordered-binary package provides a representation of JavaScript primitives, serialized into binary format (NodeJS Buffers or Uint8Arrays), such that the binary values are naturally ordered such that it matches the natural ordering or values. For example, since -2.0321 > -2.04, then `toBufferKey(-2.0321)` will be greater than `toBufferKey(-2.04)` as a binary representation, in left-to-right evaluation. This is particular useful for storing keys as binaries with something like LMDB or LevelDB, to avoid any custom sorting.
4
7
 
5
- The main module exports two functions:
8
+ The ordered-binary package supports strings, numbers, booleans, symbols, null, as well as an array of primitives. Here is an example of ordering of primitive values:
9
+ ```
10
+ Buffer.from([0]) // buffers are left unchanged, and this is the minimum value
11
+ Symbol.for('even symbols')
12
+ -10 // negative supported
13
+ -1.1 // decimals supported
14
+ 400
15
+ 3E10
16
+ 'Hello'
17
+ ['Hello', 'World']
18
+ 'World'
19
+ 'hello'
20
+ ['hello', 1, 'world']
21
+ ['hello', 'world']
22
+ Buffer.from([0xff])
23
+ ```
24
+
25
+
26
+ The main module exports these functions:
27
+
28
+ `writeKey(key: string | number | boolean | null | Array, target: Buffer, position: integer, inSequence?: boolean)` - Writes the provide key to the target buffer
29
+
30
+ `readKey(buffer, start, end, inSequence)` - Reads the key from the buffer, given the provided start and end, as a primitive value
6
31
 
7
32
  `toBufferKey(jsPrimitive)` - This accepts a string, number, or boolean as the argument, and returns a `Buffer`.
33
+
8
34
  `fromBufferKey(bufferKey, multiple)` - This accepts a Buffer and returns a JavaScript primitive value. This can also parse buffers that hold multiple values delimited by a byte `30`, by setting the second argument to true (in which case it will return an array).
package/dist/index.cjs ADDED
@@ -0,0 +1,343 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /*
6
+ control character types:
7
+ 1 - metadata
8
+ 2 - symbols
9
+ 6 - false
10
+ 7 - true
11
+ 8- 16 - negative doubles
12
+ 16-24 positive doubles
13
+ 27 - String starts with a character 27 or less or is an empty string
14
+ 0 - multipart separator
15
+ > 27 normal string characters
16
+ */
17
+ /*
18
+ * Convert arbitrary scalar values to buffer bytes with type preservation and type-appropriate ordering
19
+ */
20
+
21
+ const float64Array = new Float64Array(2);
22
+ const int32Array = new Int32Array(float64Array.buffer, 0, 4);
23
+ new Uint8Array(float64Array.buffer, 2, 6);
24
+ new Uint8Array(float64Array.buffer, 0, 8);
25
+ let nullTerminate = false;
26
+ let textEncoder;
27
+ try {
28
+ textEncoder = new TextEncoder();
29
+ } catch (error) {}
30
+
31
+ /*
32
+ * Convert arbitrary scalar values to buffer bytes with type preservation and type-appropriate ordering
33
+ */
34
+ function writeKey(key, target, position, inSequence) {
35
+ let targetView = target.dataView;
36
+ if (!targetView)
37
+ targetView = target.dataView = new DataView(target.buffer, target.byteOffset, ((target.byteLength + 3) >> 2) << 2);
38
+ switch (typeof key) {
39
+ case 'string':
40
+ let strLength = key.length;
41
+ let c1 = key.charCodeAt(0);
42
+ if (!(c1 >= 28)) // escape character
43
+ target[position++] = 27;
44
+ if (strLength < 0x40) {
45
+ let i, c2;
46
+ for (i = 0; i < strLength; i++) {
47
+ c1 = key.charCodeAt(i);
48
+ if (c1 < 0x80) {
49
+ target[position++] = c1;
50
+ } else if (c1 < 0x800) {
51
+ target[position++] = c1 >> 6 | 0xc0;
52
+ target[position++] = c1 & 0x3f | 0x80;
53
+ } else if (
54
+ (c1 & 0xfc00) === 0xd800 &&
55
+ ((c2 = key.charCodeAt(i + 1)) & 0xfc00) === 0xdc00
56
+ ) {
57
+ c1 = 0x10000 + ((c1 & 0x03ff) << 10) + (c2 & 0x03ff);
58
+ i++;
59
+ target[position++] = c1 >> 18 | 0xf0;
60
+ target[position++] = c1 >> 12 & 0x3f | 0x80;
61
+ target[position++] = c1 >> 6 & 0x3f | 0x80;
62
+ target[position++] = c1 & 0x3f | 0x80;
63
+ } else {
64
+ target[position++] = c1 >> 12 | 0xe0;
65
+ target[position++] = c1 >> 6 & 0x3f | 0x80;
66
+ target[position++] = c1 & 0x3f | 0x80;
67
+ }
68
+ }
69
+ } else if (target.utf8Write) {
70
+ position += target.utf8Write(key, position, 2000);
71
+ } else {
72
+ position += textEncoder.encodeInto(key, target.subarray(position)).written;
73
+ }
74
+ break
75
+ case 'number':
76
+ float64Array[0] = key;
77
+ let lowInt = int32Array[0];
78
+ let highInt = int32Array[1];
79
+ let length;
80
+ if (key < 0) {
81
+ targetView.setInt32(position + 4, ~((lowInt >>> 4) | (highInt << 28)));
82
+ targetView.setInt32(position + 0, (highInt ^ 0x7fffffff) >>> 4);
83
+ targetView.setInt32(position + 8, ((lowInt & 0xf) ^ 0xf) << 4, true); // just always do the null termination here
84
+ return position + 9
85
+ } else if ((lowInt & 0xf) || inSequence) {
86
+ length = 9;
87
+ } else if (lowInt & 0xfffff)
88
+ length = 8;
89
+ else if (lowInt || (highInt & 0xf))
90
+ length = 6;
91
+ else
92
+ length = 4;
93
+ // switching order to go to little endian
94
+ targetView.setInt32(position + 0, (highInt >>> 4) | 0x10000000);
95
+ targetView.setInt32(position + 4, (lowInt >>> 4) | (highInt << 28));
96
+ // if (length == 9 || nullTerminate)
97
+ targetView.setInt32(position + 8, (lowInt & 0xf) << 4, true);
98
+ return position + length;
99
+ case 'object':
100
+ if (key) {
101
+ if (key instanceof Array) {
102
+ for (let i = 0, l = key.length; i < l; i++) {
103
+ if (i > 0)
104
+ target[position++] = 0;
105
+ position = writeKey(key[i], target, position, true);
106
+ }
107
+ break
108
+ } else if (key instanceof Uint8Array) {
109
+ target.set(key, position);
110
+ position += key.length;
111
+ break
112
+ } else {
113
+ throw new Error('Unable to serialize object as a key')
114
+ }
115
+ } else // null
116
+ target[position++] = 0;
117
+ break
118
+ case 'boolean':
119
+ targetView.setUint32(position++, key ? 7 : 6, true);
120
+ return position
121
+ case 'bigint':
122
+ return writeKey(Number(key), target, position, inSequence)
123
+ case 'undefined':
124
+ return position
125
+ // undefined is interpreted as the absence of a key, signified by zero length
126
+ case 'symbol':
127
+ target[position++] = 2;
128
+ return writeKey(key.description, target, position, inSequence)
129
+ default:
130
+ throw new Error('Can not serialize key of type ' + typeof key)
131
+ }
132
+ if (nullTerminate && !inSequence)
133
+ targetView.setUint32(position, 0);
134
+ return position
135
+ }
136
+
137
+ let position;
138
+ function readKey(buffer, start, end, inSequence) {
139
+ buffer[end] = 0; // make sure it is null terminated
140
+ position = start;
141
+ let controlByte = buffer[position];
142
+ let value;
143
+ if (controlByte < 24) {
144
+ if (controlByte < 8) {
145
+ position++;
146
+ if (controlByte == 6) {
147
+ value = false;
148
+ } else if (controlByte == 7) {
149
+ value = true;
150
+ } else if (controlByte == 0) {
151
+ value = null;
152
+ } else if (controlByte == 2) {
153
+ value = Symbol.for(readString(buffer));
154
+ } else
155
+ return Uint8Array.prototype.slice.call(buffer, start, end)
156
+ } else {
157
+ let dataView = buffer.dataView || (buffer.dataView = new DataView(buffer.buffer, buffer.byteOffset, ((buffer.byteLength + 3) >> 2) << 2));
158
+ let highInt = dataView.getInt32(position) << 4;
159
+ let size = end - position;
160
+ let lowInt;
161
+ if (size > 4) {
162
+ lowInt = dataView.getInt32(position + 4);
163
+ highInt |= lowInt >>> 28;
164
+ if (size <= 6) { // clear the last bits
165
+ lowInt &= -0x1000;
166
+ }
167
+ lowInt = lowInt << 4;
168
+ if (size > 8) {
169
+ lowInt = lowInt | buffer[position + 8] >> 4;
170
+ }
171
+ } else
172
+ lowInt = 0;
173
+ if (controlByte < 16) {
174
+ // negative gets negated
175
+ highInt = highInt ^ 0x7fffffff;
176
+ lowInt = ~lowInt;
177
+ }
178
+ int32Array[1] = highInt;
179
+ int32Array[0] = lowInt;
180
+ value = float64Array[0];
181
+ position += 9;
182
+ }
183
+ } else {
184
+ if (controlByte == 27) {
185
+ position++;
186
+ }
187
+ value = readString(buffer);
188
+ /*let strStart = position
189
+ let strEnd = end
190
+ for (; position < end; position++) {
191
+ if (buffer[position] == 0) {
192
+ break
193
+ }
194
+ }
195
+ value = buffer.toString('utf8', strStart, position++)*/
196
+ }
197
+ while (position < end) {
198
+ if (buffer[position] === 0)
199
+ position++;
200
+ if (inSequence) {
201
+ encoder.position = position;
202
+ return value
203
+ }
204
+ let nextValue = readKey(buffer, position, end, true);
205
+ if (value instanceof Array) {
206
+ value.push(nextValue);
207
+ } else
208
+ value = [ value, nextValue ];
209
+ }
210
+ return value
211
+ }
212
+ const encoder = {
213
+ writeKey,
214
+ readKey,
215
+ };
216
+ const toBufferKey = (key) => {
217
+ let buffer = Buffer.alloc(2048);
218
+ return buffer.slice(0, writeKey(key, buffer, 0, 2048) + 1)
219
+ };
220
+ const fromBufferKey = (sourceBuffer) => {
221
+ return readKey(sourceBuffer, 0, sourceBuffer.length - 1)
222
+ };
223
+ const fromCharCode = String.fromCharCode;
224
+ function makeStringBuilder() {
225
+ let stringBuildCode = '(source) => {';
226
+ let previous = [];
227
+ for (let i = 0; i < 0x30; i++) {
228
+ let v = fromCharCode((i & 0xf) + 97) + fromCharCode((i >> 4) + 97);
229
+ stringBuildCode += `
230
+ let ${v} = source[position++]
231
+ if (${v} === 0)
232
+ return fromCharCode(${previous})
233
+ else if (${v} >= 0x80)
234
+ ${v} = finishUtf8(${v}, source)
235
+ `;
236
+ previous.push(v);
237
+ if (i == 1000000) // this just exists to prevent rollup from doing dead code elimination on finishUtf8
238
+ finishUtf8();
239
+ }
240
+ stringBuildCode += `return fromCharCode(${previous}) + readString(source)}`;
241
+ return stringBuildCode
242
+ }
243
+
244
+ let pendingSurrogate;
245
+ function finishUtf8(byte1, src) {
246
+ if ((byte1 & 0xe0) === 0xc0) {
247
+ // 2 bytes
248
+ const byte2 = src[position++] & 0x3f;
249
+ return ((byte1 & 0x1f) << 6) | byte2
250
+ } else if ((byte1 & 0xf0) === 0xe0) {
251
+ // 3 bytes
252
+ const byte2 = src[position++] & 0x3f;
253
+ const byte3 = src[position++] & 0x3f;
254
+ return ((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3
255
+ } else if ((byte1 & 0xf8) === 0xf0) {
256
+ // 4 bytes
257
+ if (pendingSurrogate) {
258
+ byte1 = pendingSurrogate;
259
+ pendingSurrogate = null;
260
+ position += 3;
261
+ return byte1
262
+ }
263
+ const byte2 = src[position++] & 0x3f;
264
+ const byte3 = src[position++] & 0x3f;
265
+ const byte4 = src[position++] & 0x3f;
266
+ let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
267
+ if (unit > 0xffff) {
268
+ unit -= 0x10000;
269
+ unit = 0xdc00 | (unit & 0x3ff);
270
+ pendingSurrogate = ((unit >>> 10) & 0x3ff) | 0xd800;
271
+ position -= 4; // reset so we can return the next part of the surrogate pair
272
+ }
273
+ return unit
274
+ } else {
275
+ return byte1
276
+ }
277
+ }
278
+
279
+ const enableNullTermination = () => nullTerminate = true;
280
+
281
+ const readString = eval(makeStringBuilder());
282
+
283
+ function compareKeys(a, b) {
284
+ // compare with type consistency that matches binary comparison
285
+ if (typeof a == 'object') {
286
+ if (!a) {
287
+ return b == null ? 0 : -1
288
+ }
289
+ if (a.compare) {
290
+ if (b == null) {
291
+ return 1
292
+ } else if (b.compare) {
293
+ return a.compare(b)
294
+ } else {
295
+ return -1
296
+ }
297
+ }
298
+ let arrayComparison;
299
+ if (b instanceof Array) {
300
+ let i = 0;
301
+ while((arrayComparison = compareKeys(a[i], b[i])) == 0 && i <= a.length) {
302
+ i++;
303
+ }
304
+ return arrayComparison
305
+ }
306
+ arrayComparison = compareKeys(a[0], b);
307
+ if (arrayComparison == 0 && a.length > 1)
308
+ return 1
309
+ return arrayComparison
310
+ } else if (typeof a == typeof b) {
311
+ if (typeof a === 'symbol') {
312
+ a = Symbol.keyFor(a);
313
+ b = Symbol.keyFor(b);
314
+ }
315
+ return a < b ? -1 : a === b ? 0 : 1
316
+ }
317
+ else if (typeof b == 'object') {
318
+ if (b instanceof Array)
319
+ return -compareKeys(b, a)
320
+ return 1
321
+ } else {
322
+ return typeOrder[typeof a] < typeOrder[typeof b] ? -1 : 1
323
+ }
324
+ }
325
+ const typeOrder = {
326
+ symbol: 0,
327
+ undefined: 1,
328
+ boolean: 2,
329
+ number: 3,
330
+ string: 4
331
+ };
332
+ const MINIMUM_KEY = null;
333
+ const MAXIMUM_KEY = Buffer.from([0xff]);
334
+
335
+ exports.MAXIMUM_KEY = MAXIMUM_KEY;
336
+ exports.MINIMUM_KEY = MINIMUM_KEY;
337
+ exports.compareKeys = compareKeys;
338
+ exports.enableNullTermination = enableNullTermination;
339
+ exports.encoder = encoder;
340
+ exports.fromBufferKey = fromBufferKey;
341
+ exports.readKey = readKey;
342
+ exports.toBufferKey = toBufferKey;
343
+ exports.writeKey = writeKey;
@@ -19,6 +19,11 @@ const int32Array = new Int32Array(float64Array.buffer, 0, 4)
19
19
  const uint8Array6 = new Uint8Array(float64Array.buffer, 2, 6)
20
20
  const uint8Array8 = new Uint8Array(float64Array.buffer, 0, 8)
21
21
  let nullTerminate = false
22
+ let textEncoder
23
+ try {
24
+ textEncoder = new TextEncoder()
25
+ } catch (error) {}
26
+
22
27
  /*
23
28
  * Convert arbitrary scalar values to buffer bytes with type preservation and type-appropriate ordering
24
29
  */
@@ -30,9 +35,9 @@ export function writeKey(key, target, position, inSequence) {
30
35
  case 'string':
31
36
  let strLength = key.length
32
37
  let c1 = key.charCodeAt(0)
33
- if (c1 < 28) // escape character
38
+ if (!(c1 >= 28)) // escape character
34
39
  target[position++] = 27
35
- if (strLength < 0x20) {
40
+ if (strLength < 0x40) {
36
41
  let i, c2
37
42
  for (i = 0; i < strLength; i++) {
38
43
  c1 = key.charCodeAt(i)
@@ -57,8 +62,10 @@ export function writeKey(key, target, position, inSequence) {
57
62
  target[position++] = c1 & 0x3f | 0x80
58
63
  }
59
64
  }
60
- } else {
65
+ } else if (target.utf8Write) {
61
66
  position += target.utf8Write(key, position, 2000)
67
+ } else {
68
+ position += textEncoder.encodeInto(key, target.subarray(position)).written
62
69
  }
63
70
  break
64
71
  case 'number':
@@ -214,15 +221,17 @@ function makeStringBuilder() {
214
221
  let stringBuildCode = '(source) => {'
215
222
  let previous = []
216
223
  for (let i = 0; i < 0x30; i++) {
217
- let v = fromCharCode((i & 0xf) + 97) + fromCharCode((i >> 4) + 97)
218
- stringBuildCode += `
219
- let ${v} = source[position++]
220
- if (${v} === 0)
221
- return fromCharCode(${previous})
222
- else if (${v} >= 0x80)
223
- ${v} = finishUtf8(${v}, source)
224
- `
225
- previous.push(v)
224
+ let v = fromCharCode((i & 0xf) + 97) + fromCharCode((i >> 4) + 97)
225
+ stringBuildCode += `
226
+ let ${v} = source[position++]
227
+ if (${v} === 0)
228
+ return fromCharCode(${previous})
229
+ else if (${v} >= 0x80)
230
+ ${v} = finishUtf8(${v}, source)
231
+ `
232
+ previous.push(v)
233
+ if (i == 1000000) // this just exists to prevent rollup from doing dead code elimination on finishUtf8
234
+ finishUtf8()
226
235
  }
227
236
  stringBuildCode += `return fromCharCode(${previous}) + readString(source)}`
228
237
  return stringBuildCode
@@ -268,7 +277,7 @@ export const enableNullTermination = () => nullTerminate = true
268
277
  const readString = eval(makeStringBuilder())
269
278
 
270
279
  export function compareKeys(a, b) {
271
- // compare with type consistency that matches ordered-binary
280
+ // compare with type consistency that matches binary comparison
272
281
  if (typeof a == 'object') {
273
282
  if (!a) {
274
283
  return b == null ? 0 : -1
@@ -316,3 +325,5 @@ const typeOrder = {
316
325
  number: 3,
317
326
  string: 4
318
327
  }
328
+ export const MINIMUM_KEY = null
329
+ export const MAXIMUM_KEY = Buffer.from([0xff])
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ordered-binary",
3
3
  "author": "Kris Zyp",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "description": "Conversion of JavaScript primitives to and from Buffer with binary order matching natural primitive order",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -9,19 +9,28 @@
9
9
  "url": "http://github.com/kriszyp/ordered-binary"
10
10
  },
11
11
  "scripts": {
12
+ "build": "rollup -c",
13
+ "prepare": "rollup -c",
12
14
  "test": "mocha tests -u tdd"
13
15
  },
14
16
  "type": "module",
15
- "main": "index.cjs",
16
- "module": "main.mjs",
17
- "typings": "./index.d.ts",
18
- "dependencies": {
19
- "esm": "^3.2.25"
17
+ "module": "index.js",
18
+ "exports": {
19
+ ".": {
20
+ "require": "./dist/index.cjs",
21
+ "import": "./index.js"
22
+ },
23
+ "./index.js": {
24
+ "require": "./dist/index.cjs",
25
+ "import": "./index.js"
26
+ }
20
27
  },
28
+ "typings": "./index.d.ts",
21
29
  "optionalDependencies": {},
22
30
  "devDependencies": {
23
31
  "@types/node": "latest",
24
32
  "chai": "^4",
25
- "mocha": "^4"
33
+ "mocha": "^8.1.3",
34
+ "rollup": "^1.20.3"
26
35
  }
27
36
  }
@@ -0,0 +1,11 @@
1
+ export default [
2
+ {
3
+ input: "index.js",
4
+ output: [
5
+ {
6
+ file: "dist/index.cjs",
7
+ format: "cjs"
8
+ }
9
+ ]
10
+ }
11
+ ];
package/tests/test.js CHANGED
@@ -1,6 +1,6 @@
1
- const { assert } = require('chai')
1
+ import { assert } from 'chai'
2
2
 
3
- const { toBufferKey, fromBufferKey, readKey, writeKey } = require('../index')
3
+ import { toBufferKey, fromBufferKey, readKey, writeKey } from '../index.js'
4
4
 
5
5
  function assertBufferComparison(lesser, greater) {
6
6
  for (let i = 0; i < lesser.length; i++) {
@@ -43,6 +43,7 @@ suite('key buffers', () => {
43
43
  test('string equivalence', () => {
44
44
  assert.strictEqual(fromBufferKey(toBufferKey('4')), '4')
45
45
  assert.strictEqual(fromBufferKey(toBufferKey('hello')), 'hello')
46
+ assert.strictEqual(fromBufferKey(toBufferKey('')), '')
46
47
  })
47
48
  test('string comparison', () => {
48
49
  assertBufferComparison(toBufferKey('4'), toBufferKey('5'))
package/index.cjs DELETED
@@ -1,2 +0,0 @@
1
- require = require('esm')(module)
2
- Object.assign(exports, require('./main.mjs'))